diff --git a/README-ADVANCED.md b/README-ADVANCED.md index 06b847a9..aee7398a 100644 --- a/README-ADVANCED.md +++ b/README-ADVANCED.md @@ -51,13 +51,15 @@ There are three main costs associated with AWS Lambda Power Tuning: The AWS Step Functions state machine is composed of five Lambda functions: -* **initializer**: create N versions and aliases corresponding to the power values provided as input (e.g. 128MB, 256MB, etc.) -* **executor**: execute the given Lambda function `num` times, extract execution time from logs, and compute average cost per invocation -* **cleaner**: delete all the previously generated aliases and versions -* **analyzer**: compute the optimal power value (current logic: lowest average cost per invocation) -* **optimizer**: automatically set the power to its optimal value (only if `autoOptimize` is `true`) - -Initializer, cleaner, analyzer, and optimizer are executed only once, while the executor is used by N parallel branches of the state machine - one for each configured power value. By default, the executor will execute the given Lambda function `num` consecutive times, but you can enable parallel invocation by setting `parallelInvocation` to `true`. +* **Initializer**: define all the versions and aliases that need to be created (see Publisher below) +* **Publisher**: create a new version and aliases corresponding to one of the power values provided as input (e.g. 128MB, 256MB, etc.) +* **IsCountReached**: go back to Publisher until all the versiona and aliases have been created +* **Executor**: execute the given Lambda function `num` times, extract execution time from logs, and compute average cost per invocation +* **Cleaner**: delete all the previously generated aliases and versions +* **Analyzer**: compute the optimal power value (current logic: lowest average cost per invocation) +* **Optimizer**: automatically set the power to its optimal value (only if `autoOptimize` is `true`) + +Initializer, Cleaner, Analyzer, and Optimizer are invoked only once, while the Publisher and Executor are invoked multiple times. Publisher is used in a loop to create all the required versions and aliases, which depend on the values of `num`, `powerValues`, and `onlyColdStarts`. Executor is used by N parallel branches - one for each configured power value. By default, the Executor will invike the given Lambda function `num` consecutive times, but you can enable parallel invocation by setting `parallelInvocation` to `true`. ## Weighted Payloads diff --git a/README.md b/README.md index b6df8661..ff5dcafa 100644 --- a/README.md +++ b/README.md @@ -50,26 +50,26 @@ Read more about the [deployment options here](README-DEPLOY.md). The CloudFormation template (used for option 1 to 4) accepts the following parameters: -|
**Parameter**
| Description | -|:-----------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **PowerValues**
type: _list of numbers_
default: [128,256,512,1024,1536,3008] | These power values (in MB) will be used as the default in case no `powerValues` input parameter is provided at execution time | | -| **visualizationURL**
type: _string_
default: `lambda-power-tuning.show` | The base URL for the visualization tool, you can bring your own visualization tool | -| **totalExecutionTimeout**
type: _number_
default: `300` | The timeout in seconds applied to all functions of the state machine | -| **lambdaResource**
type: _string_
default: `*` | The `Resource` used in IAM policies; it's `*` by default but you could restrict it to a prefix or a specific function ARN | -| **permissionsBoundary**
type: _string_
| The ARN of a permissions boundary (policy), applied to all functions of the state machine | -| **payloadS3Bucket**
type: _string_
| The S3 bucket name used for large payloads (>256KB); if provided, it's added to a custom managed IAM policy that grants read-only permission to the S3 bucket; more details below in the [S3 payloads section](README-ADVANCED.md#user-content-s3-payloads) | -| **payloadS3Key**
type: _string_
default: `*` | The S3 object key used for large payloads (>256KB); the default value grants access to all S3 objects in the bucket specified with `payloadS3Bucket`; more details below in the [S3 payloads section](README-ADVANCED.md#user-content-s3-payloads) | -| **layerSdkName**
type: _string_
| The name of the SDK layer, in case you need to customize it (optional) | -| **logGroupRetentionInDays**
type: _number_
default: `7` | The number of days to retain log events in the Lambda log groups. Before this parameter existed, log events were retained indefinitely | -| **securityGroupIds**
type: _list of SecurityGroup IDs_
| List of Security Groups to use in every Lambda function's VPC Configuration (optional); please note that your VPC should be configured to allow public internet access (via NAT Gateway) or include VPC Endpoints to the Lambda service | -| **subnetIds**
type: _list of Subnet IDs_
| List of Subnets to use in every Lambda function's VPC Configuration (optional); please note that your VPC should be configured to allow public internet access (via NAT Gateway) or include VPC Endpoints to the Lambda service | -| **stateMachineNamePrefix**
type: _string_
default: `powerTuningStateMachine` | Allows you to customize the name of the state machine. Maximum 43 characters, only alphanumeric (plus `-` and `_`). The last portion of the `AWS::StackId` will be appended to this value, so the full name will look like `powerTuningStateMachine-89549da0-a4f9-11ee-844d-12a2895ed91f`. Note: `StateMachineName` has a maximum of 80 characters and 36+1 from the `StackId` are appended, allowing 43 for a custom prefix. | - -Please note that the total execution time should stay below 300 seconds (5 min), which is the default timeout. You can easily estimate the total execution timeout based on the average duration of your functions. For example, if your function's average execution time is 5 seconds and you haven't enabled `parallelInvocation`, you should set `totalExecutionTimeout` to at least `num * 5`: 50 seconds if `num=10`, 500 seconds if `num=100`, and so on. If you have enabled `parallelInvocation`, usually you don't need to tune the value of `totalExecutionTimeout` unless your average execution time is above 5 min. If you have a sleep between invocations set, you should include that in your timeout calculations. - -## How to execute the state machine - -You can execute the state machine manually or programmatically, see the documentation [here](README-EXECUTE.md). +|
**Parameter**
| Description | +|:-----------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **PowerValues**
type: _list of numbers_
default: [128,256,512,1024,1536,3008] | These power values (in MB) will be used as the default in case no `powerValues` input parameter is provided at execution time. | +| **visualizationURL**
type: _string_
default: `lambda-power-tuning.show` | The base URL for the visualization tool, you can bring your own visualization tool. | +| **totalExecutionTimeout**
type: _number_
default: `300` | The timeout in seconds applied to all functions of the state machine. | +| **lambdaResource**
type: _string_
default: `*` | The `Resource` used in IAM policies; it's `*` by default but you could restrict it to a prefix or a specific function ARN. | +| **permissionsBoundary**
type: _string_
| The ARN of a permissions boundary (policy), applied to all functions of the state machine. | +| **payloadS3Bucket**
type: _string_
| The S3 bucket name used for large payloads (>256KB); if provided, it's added to a custom managed IAM policy that grants read-only permission to the S3 bucket; more details in the [S3 payloads section](README-ADVANCED.md#user-content-s3-payloads). | +| **payloadS3Key**
type: _string_
default: `*` | The S3 object key used for large payloads (>256KB); the default value grants access to all S3 objects in the bucket specified with `payloadS3Bucket`; more details in the [S3 payloads section](README-ADVANCED.md#user-content-s3-payloads). | +| **layerSdkName**
type: _string_
| The name of the SDK layer, in case you need to customize it (optional). | +| **logGroupRetentionInDays**
type: _number_
default: `7` | The number of days to retain log events in the Lambda log groups (a week by default). | +| **securityGroupIds**
type: _list of SecurityGroup IDs_
| List of Security Groups to use in every Lambda function's VPC Configuration (optional); please note that your VPC should be configured to allow public internet access (via NAT Gateway) or include VPC Endpoints to the Lambda service. | +| **subnetIds**
type: _list of Subnet IDs_
| List of Subnets to use in every Lambda function's VPC Configuration (optional); please note that your VPC should be configured to allow public internet access (via NAT Gateway) or include VPC Endpoints to the Lambda service. | +| **stateMachineNamePrefix**
type: _string_
default: `powerTuningStateMachine` | Allows you to customize the name of the state machine. Maximum 43 characters, only alphanumeric (plus `-` and `_`). The last portion of the `AWS::StackId` will be appended to this value, so the full name will look like `powerTuningStateMachine-89549da0-a4f9-11ee-844d-12a2895ed91f`. Note: `StateMachineName` has a maximum of 80 characters and 36+1 from the `StackId` are appended, allowing 43 for a custom prefix. | + +Please note that the total execution time should stay below 300 seconds (5 min), which is the default timeout. You can estimate the total execution timeout based on the average duration of your functions. For example, if your function's average execution time is 5 seconds and you haven't enabled `parallelInvocation`, you should set `totalExecutionTimeout` to at least `num * 5`: 50 seconds if `num=10`, 500 seconds if `num=100`, and so on. If you have enabled `parallelInvocation`, usually you don't need to tune the value of `totalExecutionTimeout` unless your average execution time is above 5 min. If you have set a sleep between invocations, remember to include that in your timeout calculations. + +## How to run the state machine + +You can run the state machine manually or programmatically, see the documentation [here](README-EXECUTE.md). ### State machine input (at execution time) @@ -77,25 +77,26 @@ Each execution of the state machine will require an input where you can define t |
**Parameter**
| Description | |:-----------------------------------------------------------------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **lambdaARN** (required)
type: _string_
| Unique identifier of the Lambda function you want to optimize | -| **num** (required)
type: _integer_ | The # of invocations for each power configuration (minimum 5, recommended: between 10 and 100) | -| **powerValues**
type: _string or list of integers_ | The list of power values to be tested; if not provided, the default values configured at deploy-time are used; you can provide any power values between 128MB and 10,240MB (⚠️ New AWS accounts have reduced concurrency and memory quotas (3008MB max)) | -| **payload**
type: _string, object, or list_ | The static payload that will be used for every invocation (object or string); when using a list, a weighted payload is expected in the shape of `[{"payload": {...}, "weight": X }, {"payload": {...}, "weight": Y }, {"payload": {...}, "weight": Z }]`, where the weights `X`, `Y`, and `Z` are treated as relative weights (not percentages); more details below in the [Weighted Payloads section](README-ADVANCED.md#user-content-weighted-payloads) | -| **payloadS3**
type: _string_ | A reference to Amazon S3 for large payloads (>256KB), formatted as `s3://bucket/key`; it requires read-only IAM permissions, see `payloadS3Bucket` and `payloadS3Key` below and find more details in the [S3 payloads section](README-ADVANCED.md#user-content-s3-payloads) | -| **parallelInvocation**
type: _boolean_
default: `false` | If true, all the invocations will be executed in parallel (note: depending on the value of `num`, you may experience throttling when setting `parallelInvocation` to true) | -| **strategy**
type: _string_
default: `"cost"` | It can be `"cost"` or `"speed"` or `"balanced"`; if you use `"cost"` the state machine will suggest the cheapest option (disregarding its performance), while if you use `"speed"` the state machine will suggest the fastest option (disregarding its cost). When using `"balanced"` the state machine will choose a compromise between `"cost"` and `"speed"` according to the parameter `"balancedWeight"` | -| **balancedWeight**
type: _number_
default: `0.5` | Parameter that express the trade-off between cost and time. Value is between 0 & 1, 0.0 is equivalent to `"speed"` strategy, 1.0 is equivalent to `"cost"` strategy | -| **autoOptimize**
type: _boolean_
default: `false` | If `true`, the state machine will apply the optimal configuration at the end of its execution | -| **autoOptimizeAlias**
type: _string_ | If provided - and only if `autoOptimize` if `true`, the state machine will create or update this alias with the new optimal power value | -| **dryRun**
type: _boolean_
default: `false` | If true, the state machine will execute the input function only once and it will disable every functionality related to logs analysis, auto-tuning, and visualization; the dry-run mode is intended for testing purposes, for example to verify that IAM permissions are set up correctly | -| **preProcessorARN**
type: _string_ | It must be the ARN of a Lambda function; if provided, the function will be invoked before every invocation of `lambdaARN`; more details below in the [Pre/Post-processing functions section](README-ADVANCED.md#user-content-prepost-processing-functions) | -| **postProcessorARN**
type: _string_ | It must be the ARN of a Lambda function; if provided, the function will be invoked after every invocation of `lambdaARN`; more details below in the [Pre/Post-processing functions section](README-ADVANCED.md#user-content-prepost-processing-functions) | -| **discardTopBottom**
type: _number_
default: `0.2` | By default, the state machine will discard the top/bottom 20% of "outliers" (the fastest and slowest), to filter out the effects of cold starts that would bias the overall averages. You can customize this parameter by providing a value between 0 and 0.4, with 0 meaning no results are discarded and 0.4 meaning that 40% of the top/bottom results are discarded (i.e. only 20% of the results are considered). | -| **sleepBetweenRunsMs**
type: _integer_ | If provided, the time in milliseconds that the tuner function will sleep/wait after invoking your function, but before carrying out the Post-Processing step, should that be provided. This could be used if you have aggressive downstream rate limits you need to respect. By default this will be set to 0 and the function won't sleep between invocations. Setting this value will have no effect if running the invocations in parallel. | -| **disablePayloadLogs**
type: _boolean_
default: `false` | If provided and set to a truthy value, suppresses `payload` from error messages and logs. If `preProcessorARN` is provided, this also suppresses the output payload of the pre-processor. | -| **includeOutputResults**
type: _boolean_
default: `false` | If provided and set to true, the average cost and average duration for every power value configuration will be included in the state machine output. | - -Here's a typical execution input with basic parameters: +| **lambdaARN** (required)
type: _string_
| Unique identifier of the Lambda function you want to optimize. | +| **num** (required)
type: _integer_ | The # of invocations for each power configuration (minimum 5, recommended: between 10 and 100). | +| **powerValues**
type: _string or list of integers_ | The list of power values to be tested; if not provided, the default values configured at deploy-time are used; you can provide any power values between 128MB and 10,240MB. ⚠️ New AWS accounts have reduced concurrency and memory quotas (3008MB max). | +| **payload**
type: _string, object, or list_ | The static payload that will be used for every invocation (object or string); when using a list, a weighted payload is expected in the shape of `[{"payload": {...}, "weight": X }, {"payload": {...}, "weight": Y }, {"payload": {...}, "weight": Z }]`, where the weights `X`, `Y`, and `Z` are treated as relative weights (not percentages); more details in the [Weighted Payloads section](README-ADVANCED.md#user-content-weighted-payloads). | +| **payloadS3**
type: _string_ | An Amazon S3 object reference for large payloads (>256KB), formatted as `s3://bucket/key`; it requires read-only IAM permissions, see `payloadS3Bucket` and `payloadS3Key` below and find more details in the [S3 payloads section](README-ADVANCED.md#user-content-s3-payloads). | +| **parallelInvocation**
type: _boolean_
default: `false` | If true, all the invocations will run in parallel. ⚠️ Note: depending on the value of `num`, you might experience throttling. | +| **strategy**
type: _string_
default: `"cost"` | It can be `"cost"` or `"speed"` or `"balanced"`; if you use `"cost"` the state machine will suggest the cheapest option (disregarding its performance), while if you use `"speed"` the state machine will suggest the fastest option (disregarding its cost). When using `"balanced"` the state machine will choose a compromise between `"cost"` and `"speed"` according to the parameter `"balancedWeight"`. | +| **balancedWeight**
type: _number_
default: `0.5` | Parameter that represents the trade-off between cost and speed. Value is between 0 and 1, where 0.0 is equivalent to `"speed"` strategy, 1.0 is equivalent to `"cost"` strategy. | +| **autoOptimize**
type: _boolean_
default: `false` | If true, the state machine will apply the optimal configuration at the end of its execution. | +| **autoOptimizeAlias**
type: _string_ | If provided - and only if `autoOptimize` is true, the state machine will create or update this alias with the new optimal power value. | +| **dryRun**
type: _boolean_
default: `false` | If true, the state machine will invoke the input function only once and disable every functionality related to logs analysis, auto-tuning, and visualization; this is intended for testing purposes, for example to verify that IAM permissions are set up correctly. | +| **preProcessorARN**
type: _string_ | The ARN of a Lambda function that will be invoked before every invocation of `lambdaARN`; more details in the [Pre/Post-processing functions section](README-ADVANCED.md#user-content-prepost-processing-functions). | +| **postProcessorARN**
type: _string_ | The ARN of a Lambda function that will be invoked after every invocation of `lambdaARN`; more details in the [Pre/Post-processing functions section](README-ADVANCED.md#user-content-prepost-processing-functions). | +| **discardTopBottom**
type: _number_
default: `0.2` | By default, the state machine will discard the top/bottom 20% of "outlier invocations" (the fastest and slowest) to filter out the effects of cold starts and remove any bias from overall averages. You can customize this parameter by providing a value between 0 and 0.4, where 0 means no results are discarded and 0.4 means 40% of the top/bottom results are discarded (i.e. only 20% of the results are considered). | +| **sleepBetweenRunsMs**
type: _integer_ | If provided, the time in milliseconds that the tuner will sleep/wait after invoking your function, but before carrying out the Post-Processing step, should that be provided. This could be used if you have aggressive downstream rate limits you need to respect. By default this will be set to 0 and the function won't sleep between invocations. This has no effect if running the invocations in parallel. | +| **disablePayloadLogs**
type: _boolean_
default: `false` | If true, suppresses `payload` from error messages and logs. If `preProcessorARN` is provided, this also suppresses the output payload of the pre-processor. | +| **includeOutputResults**
type: _boolean_
default: `false` | If true, the average cost and average duration for every power value configuration will be included in the state machine output. | +| **onlyColdStarts**
type: _boolean_
default: `false` | If true, the tool will force all invocations to be cold starts. The initialization phase will be considerably slower as `num` versions/aliases need to be created for each power value. | + +Here's a typical state machine input with basic parameters: ```json { diff --git a/imgs/state-machine-screenshot.png b/imgs/state-machine-screenshot.png index bc1eda09..1ed27b99 100644 Binary files a/imgs/state-machine-screenshot.png and b/imgs/state-machine-screenshot.png differ diff --git a/lambda/analyzer.js b/lambda/analyzer.js index 78e9d355..d2890550 100644 --- a/lambda/analyzer.js +++ b/lambda/analyzer.js @@ -61,10 +61,12 @@ const findOptimalConfiguration = (event) => { const balancedWeight = getBalancedWeight(event); const optimizationFunction = optimizationStrategies[strategy](); const optimal = optimizationFunction(stats, balancedWeight); + const onlyColdStarts = event.onlyColdStarts; + const num = event.num; // also compute total cost of optimization state machine & lambda optimal.stateMachine = {}; - optimal.stateMachine.executionCost = utils.stepFunctionsCost(event.stats.length); + optimal.stateMachine.executionCost = utils.stepFunctionsCost(event.stats.length, onlyColdStarts, num); optimal.stateMachine.lambdaCost = stats .map((p) => p.totalCost) .reduce((a, b) => a + b, 0); diff --git a/lambda/cleaner.js b/lambda/cleaner.js index fef8260b..89b7145d 100644 --- a/lambda/cleaner.js +++ b/lambda/cleaner.js @@ -8,13 +8,20 @@ const utils = require('./utils'); */ module.exports.handler = async(event, context) => { - const {lambdaARN, powerValues} = event; + const { + lambdaARN, + powerValues, + onlyColdStarts, + num, + } = extractDataFromInput(event); validateInput(lambdaARN, powerValues); // may throw - const ops = powerValues.map(async(value) => { - const alias = 'RAM' + value; - await cleanup(lambdaARN, alias); // may throw + // build list of aliases to clean up + const aliases = buildAliasListForCleanup(lambdaARN, onlyColdStarts, powerValues, num); + + const ops = aliases.map(async(alias) => { + await cleanup(lambdaARN, alias); }); // run everything in parallel and wait until completed @@ -23,12 +30,32 @@ module.exports.handler = async(event, context) => { return 'OK'; }; +const buildAliasListForCleanup = (lambdaARN, onlyColdStarts, powerValues, num) => { + if (onlyColdStarts){ + return powerValues.map((powerValue) => { + return utils.range(num).map((index) => { + return utils.buildAliasString(`RAM${powerValue}`, onlyColdStarts, index); + }); + }).flat(); + } + return powerValues.map((powerValue) => utils.buildAliasString(`RAM${powerValue}`)); +}; + +const extractDataFromInput = (event) => { + return { + lambdaARN: event.lambdaARN, + powerValues: event.lambdaConfigurations.powerValues, + onlyColdStarts: event.onlyColdStarts, + num: parseInt(event.num, 10), // parse as we do in the initializer + }; +}; + const validateInput = (lambdaARN, powerValues) => { if (!lambdaARN) { throw new Error('Missing or empty lambdaARN'); } if (!powerValues || !powerValues.length) { - throw new Error('Missing or empty power values'); + throw new Error('Missing or empty powerValues values'); } }; diff --git a/lambda/executor.js b/lambda/executor.js index 01f4e339..9b7034b0 100644 --- a/lambda/executor.js +++ b/lambda/executor.js @@ -20,6 +20,7 @@ module.exports.handler = async(event, context) => { preProcessorARN, postProcessorARN, discardTopBottom, + onlyColdStarts, sleepBetweenRunsMs, disablePayloadLogs, } = await extractDataFromInput(event); @@ -35,8 +36,11 @@ module.exports.handler = async(event, context) => { const lambdaAlias = 'RAM' + value; let results; - // fetch architecture from $LATEST - const {architecture, isPending} = await utils.getLambdaConfig(lambdaARN, lambdaAlias); + // defaulting the index to 0 as the index is required for onlyColdStarts + let aliasToInvoke = utils.buildAliasString(lambdaAlias, onlyColdStarts, 0); + // We need the architecture, regardless of onlyColdStarts or not + const {architecture, isPending} = await utils.getLambdaConfig(lambdaARN, aliasToInvoke); + console.log(`Detected architecture type: ${architecture}, isPending: ${isPending}`); // pre-generate an array of N payloads @@ -49,12 +53,14 @@ module.exports.handler = async(event, context) => { payloads: payloads, preARN: preProcessorARN, postARN: postProcessorARN, + onlyColdStarts: onlyColdStarts, sleepBetweenRunsMs: sleepBetweenRunsMs, disablePayloadLogs: disablePayloadLogs, }; // wait if the function/alias state is Pending - if (isPending) { + // in the case of onlyColdStarts, we will verify each alias in the runInParallel or runInSeries + if (isPending && !onlyColdStarts) { await utils.waitForAliasActive(lambdaARN, lambdaAlias); console.log('Alias active'); } @@ -97,8 +103,14 @@ const extractDiscardTopBottomValue = (event) => { // extract discardTopBottom used to trim values from average duration let discardTopBottom = event.discardTopBottom; if (typeof discardTopBottom === 'undefined') { + // default value for discardTopBottom discardTopBottom = 0.2; } + // In case of onlyColdStarts, we only have 1 invocation per alias, therefore we shouldn't discard any execution + if (event.onlyColdStarts){ + discardTopBottom = 0; + console.log('Setting discardTopBottom to 0, every invocation should be accounted when onlyColdStarts'); + } // discardTopBottom must be between 0 and 0.4 return Math.min(Math.max(discardTopBottom, 0.0), 0.4); }; @@ -128,16 +140,22 @@ const extractDataFromInput = async(event) => { preProcessorARN: input.preProcessorARN, postProcessorARN: input.postProcessorARN, discardTopBottom: discardTopBottom, + onlyColdStarts: !!input.onlyColdStarts, sleepBetweenRunsMs: sleepBetweenRunsMs, disablePayloadLogs: !!input.disablePayloadLogs, }; }; -const runInParallel = async({num, lambdaARN, lambdaAlias, payloads, preARN, postARN, disablePayloadLogs}) => { +const runInParallel = async({num, lambdaARN, lambdaAlias, payloads, preARN, postARN, disablePayloadLogs, onlyColdStarts}) => { const results = []; // run all invocations in parallel ... const invocations = utils.range(num).map(async(_, i) => { - const {invocationResults, actualPayload} = await utils.invokeLambdaWithProcessors(lambdaARN, lambdaAlias, payloads[i], preARN, postARN, disablePayloadLogs); + let aliasToInvoke = utils.buildAliasString(lambdaAlias, onlyColdStarts, i); + if (onlyColdStarts){ + await utils.waitForAliasActive(lambdaARN, aliasToInvoke); + console.log(`${aliasToInvoke} is active`); + } + const {invocationResults, actualPayload} = await utils.invokeLambdaWithProcessors(lambdaARN, aliasToInvoke, payloads[i], preARN, postARN, disablePayloadLogs); // invocation errors return 200 and contain FunctionError and Payload if (invocationResults.FunctionError) { let errorMessage = 'Invocation error (running in parallel)'; @@ -150,11 +168,16 @@ const runInParallel = async({num, lambdaARN, lambdaAlias, payloads, preARN, post return results; }; -const runInSeries = async({num, lambdaARN, lambdaAlias, payloads, preARN, postARN, sleepBetweenRunsMs, disablePayloadLogs}) => { +const runInSeries = async({num, lambdaARN, lambdaAlias, payloads, preARN, postARN, sleepBetweenRunsMs, disablePayloadLogs, onlyColdStarts}) => { const results = []; for (let i = 0; i < num; i++) { + let aliasToInvoke = utils.buildAliasString(lambdaAlias, onlyColdStarts, i); // run invocations in series - const {invocationResults, actualPayload} = await utils.invokeLambdaWithProcessors(lambdaARN, lambdaAlias, payloads[i], preARN, postARN, disablePayloadLogs); + if (onlyColdStarts){ + await utils.waitForAliasActive(lambdaARN, aliasToInvoke); + console.log(`${aliasToInvoke} is active`); + } + const {invocationResults, actualPayload} = await utils.invokeLambdaWithProcessors(lambdaARN, aliasToInvoke, payloads[i], preARN, postARN, disablePayloadLogs); // invocation errors return 200 and contain FunctionError and Payload if (invocationResults.FunctionError) { let errorMessage = 'Invocation error (running in series)'; @@ -169,18 +192,19 @@ const runInSeries = async({num, lambdaARN, lambdaAlias, payloads, preARN, postAR }; const computeStatistics = (baseCost, results, value, discardTopBottom) => { - // use results (which include logs) to compute average duration ... - - const durations = utils.parseLogAndExtractDurations(results); - const averageDuration = utils.computeAverageDuration(durations, discardTopBottom); + // use results (which include logs) to compute average duration ... + const totalDurations = utils.parseLogAndExtractDurations(results); + const averageDuration = utils.computeAverageDuration(totalDurations, discardTopBottom); console.log('Average duration: ', averageDuration); - // ... and overall statistics - const averagePrice = utils.computePrice(baseCost, minRAM, value, averageDuration); - + // ... and overall cost statistics + const billedDurations = utils.parseLogAndExtractBilledDurations(results); + const averageBilledDuration = utils.computeAverageDuration(billedDurations, discardTopBottom); + console.log('Average Billed duration: ', averageBilledDuration); + const averagePrice = utils.computePrice(baseCost, minRAM, value, averageBilledDuration); // .. and total cost (exact $) - const totalCost = utils.computeTotalCost(baseCost, minRAM, value, durations); + const totalCost = utils.computeTotalCost(baseCost, minRAM, value, billedDurations); const stats = { averagePrice, diff --git a/lambda/initializer.js b/lambda/initializer.js index bfd6a3c8..56e422b3 100644 --- a/lambda/initializer.js +++ b/lambda/initializer.js @@ -8,24 +8,58 @@ const defaultPowerValues = process.env.defaultPowerValues.split(','); */ module.exports.handler = async(event, context) => { - const {lambdaARN, num} = event; - const powerValues = extractPowerValues(event); + const { + lambdaARN, + num, + powerValues, + onlyColdStarts, + } = extractDataFromInput(event); validateInput(lambdaARN, num); // may throw // fetch initial $LATEST value so we can reset it later - const initialPower = await utils.getLambdaPower(lambdaARN); + const {power, description} = await utils.getLambdaPower(lambdaARN); + console.log(power, description); + + let initConfigurations = []; // reminder: configuration updates must run sequentially // (otherwise you get a ResourceConflictException) - for (let value of powerValues){ - const alias = 'RAM' + value; - await utils.createPowerConfiguration(lambdaARN, value, alias); + for (let powerValue of powerValues){ + const baseAlias = 'RAM' + powerValue; + if (!onlyColdStarts){ + initConfigurations.push({powerValue: powerValue, alias: baseAlias}); + } else { + for (let n of utils.range(num)){ + let alias = utils.buildAliasString(baseAlias, onlyColdStarts, n); + // here we inject a custom description to force the creation of a new version + // even if the power is the same, which will force a cold start + initConfigurations.push({powerValue: powerValue, alias: alias, description: `${description} - ${alias}`}); + } + } } + // Publish another version to revert the Lambda Function to its original configuration + initConfigurations.push({powerValue: power, description: description}); + + return { + initConfigurations: initConfigurations, + iterator: { + index: 0, + count: initConfigurations.length, + continue: true, + }, + powerValues: powerValues, + }; +}; - await utils.setLambdaPower(lambdaARN, initialPower); - return powerValues; +const extractDataFromInput = (event) => { + return { + lambdaARN: event.lambdaARN, + num: parseInt(event.num, 10), + powerValues: extractPowerValues(event), + onlyColdStarts: !!event.onlyColdStarts, + }; }; const extractPowerValues = (event) => { diff --git a/lambda/publisher.js b/lambda/publisher.js new file mode 100644 index 00000000..f1743cfe --- /dev/null +++ b/lambda/publisher.js @@ -0,0 +1,48 @@ +'use strict'; + +const utils = require('./utils'); + + +module.exports.handler = async(event, context) => { + const {lambdaConfigurations, currConfig, lambdaARN} = validateInputs(event); + const currentIterator = lambdaConfigurations.iterator; + // publish version & assign alias (if present) + await utils.createPowerConfiguration(lambdaARN, currConfig.powerValue, currConfig.alias, currConfig.description); + + const result = { + powerValues: lambdaConfigurations.powerValues, + initConfigurations: lambdaConfigurations.initConfigurations, + iterator: { + index: (currentIterator.index + 1), + count: currentIterator.count, + continue: ((currentIterator.index + 1) < currentIterator.count), + }, + }; + + if (!result.iterator.continue) { + // clean the list of configuration if we're done iterating + delete result.initConfigurations; + } + + return result; +}; +function validateInputs(event) { + if (!event.lambdaARN) { + throw new Error('Missing or empty lambdaARN'); + } + const lambdaARN = event.lambdaARN; + if (!(event.lambdaConfigurations && event.lambdaConfigurations.iterator && event.lambdaConfigurations.initConfigurations)){ + throw new Error('Invalid iterator for initialization'); + } + const iterator = event.lambdaConfigurations.iterator; + if (!(iterator.index >= 0 && iterator.index < iterator.count)){ + throw new Error(`Invalid iterator index: ${iterator.index}`); + } + const lambdaConfigurations = event.lambdaConfigurations; + const currIdx = iterator.index; + const currConfig = lambdaConfigurations.initConfigurations[currIdx]; + if (!(currConfig && currConfig.powerValue)){ + throw new Error(`Invalid init configuration: ${JSON.stringify(currConfig)}`); + } + return {lambdaConfigurations, currConfig, lambdaARN}; +} diff --git a/lambda/utils.js b/lambda/utils.js index 54e09e0b..745c58dc 100644 --- a/lambda/utils.js +++ b/lambda/utils.js @@ -13,8 +13,35 @@ const url = require('url'); // local reference to this module const utils = module.exports; +const DURATIONS = { + durationMs: 'durationMs', + initDurationMs: 'initDurationMs', + restoreDurationMs: 'restoreDurationMs', + billedDurationMs: 'billedDurationMs', + billedRestoreDurationMs: 'billedRestoreDurationMs', +}; +module.exports.DURATIONS = DURATIONS; + // cost of 6+N state transitions (AWS Step Functions) -module.exports.stepFunctionsCost = (nPower) => +(this.stepFunctionsBaseCost() * (6 + nPower)).toFixed(5); +/** + * Computes the cost for all state transitions in this state machine execution + */ +module.exports.stepFunctionsCost = (nPower, onlyColdStarts, num) => { + const baseCostPerTransition = this.stepFunctionsBaseCost(); + + // 6 -> number of default state transition (no matter what) + // nPower * 3 -> these are invoked nPower times: Executor + Publisher + IsCountReached + var multiplier = 6 + nPower * 3; + + if (onlyColdStarts) { + // 6 -> number of default state transition (no matter what) + // nPower -> number of Executor branches happening in parallel + // 2 * nPower * num -> number of loops (Publisher + IsCountReached) + multiplier = 6 + nPower + 2 * nPower * num; + } + + return +(baseCostPerTransition * multiplier).toFixed(5); +}; module.exports.stepFunctionsBaseCost = () => { const prices = JSON.parse(process.env.sfCosts); @@ -31,6 +58,14 @@ module.exports.lambdaBaseCost = (region, architecture) => { return this.baseCostForRegion(priceMap, region); }; +module.exports.buildAliasString = (baseAlias, onlyColdStarts, index) => { + let alias = baseAlias; + if (onlyColdStarts) { + alias += `-${index}`; + } + return alias; +}; + module.exports.allPowerValues = () => { const increment = 64; const powerValues = []; @@ -76,14 +111,19 @@ module.exports.verifyAliasExistance = async(lambdaARN, alias) => { /** * Update power, publish new version, and create/update alias. */ -module.exports.createPowerConfiguration = async(lambdaARN, value, alias) => { +module.exports.createPowerConfiguration = async(lambdaARN, value, alias, description) => { try { - await utils.setLambdaPower(lambdaARN, value); + await utils.setLambdaPower(lambdaARN, value, description); - // wait for functoin update to complete + // wait for function update to complete await utils.waitForFunctionUpdate(lambdaARN); const {Version} = await utils.publishLambdaVersion(lambdaARN); + // alias is not passed in when restoring to the original Lambda configuration + if (typeof alias === 'undefined'){ + console.log('No alias defined'); + return; + } const aliasExists = await utils.verifyAliasExistance(lambdaARN, alias); if (aliasExists) { await utils.updateLambdaAlias(lambdaARN, alias, Version); @@ -142,7 +182,11 @@ module.exports.getLambdaPower = async(lambdaARN) => { }; const lambda = utils.lambdaClientFromARN(lambdaARN); const config = await lambda.send(new GetFunctionConfigurationCommand(params)); - return config.MemorySize; + return { + power: config.MemorySize, + // we need to fetch env vars only to add a new one and force a cold start + description: config.Description, + }; }; /** @@ -177,11 +221,14 @@ module.exports.getLambdaConfig = async(lambdaARN, alias) => { /** * Update a given Lambda Function's memory size (always $LATEST version). */ -module.exports.setLambdaPower = (lambdaARN, value) => { +module.exports.setLambdaPower = (lambdaARN, value, description) => { console.log('Setting power to ', value); const params = { FunctionName: lambdaARN, MemorySize: parseInt(value, 10), + // the Description field is used as a way to force new versions being published. + // this is required when using Power Tuning with the onlyColdStart flag + Description: description, }; const lambda = utils.lambdaClientFromARN(lambdaARN); return lambda.send(new UpdateFunctionConfigurationCommand(params)); @@ -490,7 +537,24 @@ module.exports.computePrice = (minCost, minRAM, value, duration) => { module.exports.parseLogAndExtractDurations = (data) => { return data.map(log => { const logString = utils.base64decode(log.LogResult || ''); - return utils.extractDuration(logString); + // Total duration = duration + (initDuration or restoreDuration) + // restoreDuration is present for SnapStart-enabled Lambda functions + // initDuration is present for non-SnapStart Lambda functions + // if either is missing, we assume it's 0 + return utils.extractDuration(logString, DURATIONS.durationMs) + + utils.extractDuration(logString, DURATIONS.initDurationMs) + + utils.extractDuration(logString, DURATIONS.restoreDurationMs); + }); +}; +module.exports.parseLogAndExtractBilledDurations = (data) => { + return data.map(log => { + const logString = utils.base64decode(log.LogResult || ''); + // Total billed duration = billedDuration + billedRestoreDuration + // billedDuration is present for all Lambda functions + // billedRestoreDuration is present for SnapStart-enabled Lambda functions + // if billedRestoreDuration is missing, we assume it's 0 + return utils.extractDuration(logString, DURATIONS.billedDurationMs) + + utils.extractDuration(logString, DURATIONS.billedRestoreDurationMs); }); }; @@ -543,31 +607,52 @@ module.exports.computeAverageDuration = (durations, discardTopBottom) => { /** * Extract duration (in ms) from a given Lambda's CloudWatch log. */ -module.exports.extractDuration = (log) => { +module.exports.extractDuration = (log, durationType) => { + if (!durationType){ + durationType = DURATIONS.durationMs; // default to `durationMs` + } if (log.charAt(0) === '{') { // extract from JSON (multi-line) - return utils.extractDurationFromJSON(log); + return utils.extractDurationFromJSON(log, durationType); } else { // extract from text - return utils.extractDurationFromText(log); + return utils.extractDurationFromText(log, durationType); } }; +function getRegex(durationType) { + switch (durationType) { + case DURATIONS.billedDurationMs: + return /\tBilled Duration: (\d+) ms/m; + case DURATIONS.initDurationMs: + return /\tInit Duration: (\d+\.\d+) ms/m; + case DURATIONS.durationMs: + return /\tDuration: (\d+\.\d+) ms/m; + case DURATIONS.restoreDurationMs: + return /\tRestore Duration: (\d+\.\d+) ms/m; + case DURATIONS.billedRestoreDurationMs: + return /\tBilled Restore Duration: (\d+) ms/m; + default: + throw new Error(`Unknown duration type: ${durationType}`); + } +} + /** - * Extract duration (in ms) from a given text log. + * Extract duration (in ms) from a given text log and duration type. */ -module.exports.extractDurationFromText = (log) => { - const regex = /\tBilled Duration: (\d+) ms/m; - const match = regex.exec(log); +module.exports.extractDurationFromText = (log, durationType) => { + let regex = getRegex(durationType); + const match = regex.exec(log); + // Default to 0 if the specific duration is not found in the log line if (match == null) return 0; - return parseInt(match[1], 10); + return parseFloat(match[1], 10); }; /** - * Extract duration (in ms) from a given JSON log (multi-line). + * Extract duration (in ms) from a given JSON log (multi-line) and duration type. */ -module.exports.extractDurationFromJSON = (log) => { +module.exports.extractDurationFromJSON = (log, durationType) => { // extract each line and parse it to JSON object const lines = log.split('\n').filter((line) => line.startsWith('{')).map((line) => { try { @@ -580,12 +665,15 @@ module.exports.extractDurationFromJSON = (log) => { // find the log corresponding to the invocation report const durationLine = lines.find((line) => line.type === 'platform.report'); if (durationLine){ - return durationLine.record.metrics.billedDurationMs; + let field = durationType; + // Default to 0 if the specific duration is not found in the log line + return durationLine.record.metrics[field] || 0; } throw new Error('Unrecognized JSON log'); }; + /** * Encode a given string to base64. */ @@ -614,6 +702,7 @@ module.exports.lambdaClientFromARN = (lambdaARN) => { const region = this.regionFromARN(lambdaARN); return new LambdaClient({ region, + maxAttempts: 20, requestTimeout: 15 * 60 * 1000, }); }; diff --git a/statemachine/statemachine.asl.json b/statemachine/statemachine.asl.json index 30fb2307..c2c23a11 100644 --- a/statemachine/statemachine.asl.json +++ b/statemachine/statemachine.asl.json @@ -5,21 +5,37 @@ "Initializer": { "Type": "Task", "Resource": "${initializerArn}", - "Next": "Branching", - "ResultPath": "$.powerValues", + "Next": "Publisher", + "ResultPath": "$.lambdaConfigurations", + "TimeoutSeconds": ${totalExecutionTimeout} + }, + "Publisher": { + "Type": "Task", + "Resource": "${publisherArn}", + "Next": "IsCountReached", + "ResultPath": "$.lambdaConfigurations", "TimeoutSeconds": ${totalExecutionTimeout}, - "Catch": [ + "Catch": [{ + "ErrorEquals": [ "States.ALL" ], + "Next": "CleanUpOnError", + "ResultPath": "$.error" + }] + }, + "IsCountReached": { + "Type": "Choice", + "Choices": [ { - "ErrorEquals": ["States.ALL"], - "Next": "CleanUpOnError", - "ResultPath": "$.error" + "Variable": "$.lambdaConfigurations.iterator.continue", + "BooleanEquals": true, + "Next": "Publisher" } - ] + ], + "Default": "Branching" }, "Branching": { "Type": "Map", "Next": "Cleaner", - "ItemsPath": "$.powerValues", + "ItemsPath": "$.lambdaConfigurations.powerValues", "ResultPath": "$.stats", "ItemSelector": { "input.$": "$", @@ -83,4 +99,4 @@ "End": true } } -} \ No newline at end of file +} diff --git a/template.yml b/template.yml index 664ae38a..3176f679 100644 --- a/template.yml +++ b/template.yml @@ -123,6 +123,21 @@ Resources: Properties: CodeUri: lambda Handler: initializer.handler + Layers: + - !Ref SDKlayer + Policies: + - AWSLambdaBasicExecutionRole # Only logs + - Version: '2012-10-17' # allow Lambda actions + Statement: + - Effect: Allow + Action: + - lambda:GetFunctionConfiguration + Resource: !Ref lambdaResource + publisher: + Type: AWS::Serverless::Function + Properties: + CodeUri: lambda + Handler: publisher.handler Layers: - !Ref SDKlayer Policies: @@ -278,6 +293,7 @@ Resources: DefinitionUri: statemachine/statemachine.asl.json DefinitionSubstitutions: initializerArn: !GetAtt initializer.Arn + publisherArn: !GetAtt publisher.Arn executorArn: !GetAtt executor.Arn cleanerArn: !GetAtt cleaner.Arn analyzerArn: !GetAtt analyzer.Arn @@ -286,4 +302,4 @@ Resources: Outputs: StateMachineARN: - Value: !Ref powerTuningStateMachine \ No newline at end of file + Value: !Ref powerTuningStateMachine diff --git a/terraform/module/json_files/executor.json b/terraform/module/json_files/executor.json index 344235b2..758d7a53 100644 --- a/terraform/module/json_files/executor.json +++ b/terraform/module/json_files/executor.json @@ -10,4 +10,4 @@ "Resource": "arn:aws:lambda:*:${account_id}:function:*" } ] -} \ No newline at end of file +} diff --git a/terraform/module/json_files/initializer.json b/terraform/module/json_files/initializer.json index 4f3b81c6..f2365737 100644 --- a/terraform/module/json_files/initializer.json +++ b/terraform/module/json_files/initializer.json @@ -4,12 +4,7 @@ { "Effect": "Allow", "Action": [ - "lambda:GetAlias", - "lambda:GetFunctionConfiguration", - "lambda:PublishVersion", - "lambda:UpdateFunctionConfiguration", - "lambda:CreateAlias", - "lambda:UpdateAlias" + "lambda:GetFunctionConfiguration" ], "Resource": "arn:aws:lambda:*:${account_id}:function:*" } diff --git a/terraform/module/json_files/publisher.json b/terraform/module/json_files/publisher.json new file mode 100644 index 00000000..53ea18a2 --- /dev/null +++ b/terraform/module/json_files/publisher.json @@ -0,0 +1,17 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "lambda:GetAlias", + "lambda:GetFunctionConfiguration", + "lambda:PublishVersion", + "lambda:UpdateFunctionConfiguration", + "lambda:CreateAlias", + "lambda:UpdateAlias" + ], + "Resource": "arn:aws:lambda:*:${account_id}:function:*" + } + ] +} \ No newline at end of file diff --git a/terraform/module/json_files/state_machine.json b/terraform/module/json_files/state_machine.json index e63cc5d4..bcb6bd8e 100644 --- a/terraform/module/json_files/state_machine.json +++ b/terraform/module/json_files/state_machine.json @@ -5,19 +5,44 @@ "Initializer": { "Type": "Task", "Resource": "${initializerArn}", - "Next": "Branching", - "ResultPath": "$.powerValues", + "Next": "Publisher", + "ResultPath": "$.lambdaConfigurations", + "TimeoutSeconds": 600, + "Catch": [ + { + "ErrorEquals": ["States.ALL"], + "Next": "CleanUpOnError", + "ResultPath": "$.error" + } + ] + }, + "Publisher": { + "Type": "Task", + "Resource": "${publisherArn}", + "Next": "IsCountReached", + "ResultPath": "$.lambdaConfigurations", "TimeoutSeconds": 600, "Catch": [{ - "ErrorEquals": [ "States.ALL" ], - "Next": "CleanUpOnError", - "ResultPath": "$.error" + "ErrorEquals": [ "States.ALL" ], + "Next": "CleanUpOnError", + "ResultPath": "$.error" }] }, + "IsCountReached": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.lambdaConfigurations.iterator.continue", + "BooleanEquals": true, + "Next": "Publisher" + } + ], + "Default": "Branching" + }, "Branching": { "Type": "Map", "Next": "Cleaner", - "ItemsPath": "$.powerValues", + "ItemsPath": "$.lambdaConfigurations.powerValues", "ResultPath": "$.stats", "ItemSelector": { "input.$": "$", @@ -77,4 +102,4 @@ "End": true } } -} \ No newline at end of file +} diff --git a/terraform/module/lambda.tf b/terraform/module/lambda.tf index 9e134921..7db7f542 100644 --- a/terraform/module/lambda.tf +++ b/terraform/module/lambda.tf @@ -147,6 +147,43 @@ resource "aws_lambda_function" "initializer" { depends_on = [aws_lambda_layer_version.lambda_layer] } +resource "aws_lambda_function" "publisher" { + filename = "../src/app.zip" + function_name = "${var.lambda_function_prefix}-publisher" + role = aws_iam_role.publisher_role.arn + handler = "publisher.handler" + layers = [aws_lambda_layer_version.lambda_layer.arn] + memory_size = 128 + timeout = 300 + + # The filebase64sha256() function is available in Terraform 0.11.12 and later + # For Terraform 0.11.11 and earlier, use the base64sha256() function and the file() function: + # source_code_hash = "${base64sha256(file("lambda_function_payload.zip"))}" + source_code_hash = data.archive_file.app.output_base64sha256 + + runtime = "nodejs20.x" + + dynamic "vpc_config" { + for_each = var.vpc_subnet_ids != null && var.vpc_security_group_ids != null ? [true] : [] + content { + security_group_ids = var.vpc_security_group_ids + subnet_ids = var.vpc_subnet_ids + } + } + + environment { + variables = { + defaultPowerValues = local.defaultPowerValues, + minRAM = local.minRAM, + baseCosts = local.baseCosts, + sfCosts = local.sfCosts, + visualizationURL = local.visualizationURL + } + } + + depends_on = [aws_lambda_layer_version.lambda_layer] +} + resource "aws_lambda_function" "optimizer" { filename = "../src/app.zip" function_name = "${var.lambda_function_prefix}-optimizer" diff --git a/terraform/module/locals.tf b/terraform/module/locals.tf index e7efa4f1..0d592f18 100644 --- a/terraform/module/locals.tf +++ b/terraform/module/locals.tf @@ -11,6 +11,7 @@ locals { "${path.module}/json_files/state_machine.json", { initializerArn = aws_lambda_function.initializer.arn, + publisherArn = aws_lambda_function.publisher.arn, executorArn = aws_lambda_function.executor.arn, cleanerArn = aws_lambda_function.cleaner.arn, analyzerArn = aws_lambda_function.analyzer.arn, @@ -39,6 +40,13 @@ locals { } ) + publisher_template = templatefile( + "${path.module}/json_files/publisher.json", + { + account_id = var.account_id + } + ) + optimizer_template = templatefile( "${path.module}/json_files/optimizer.json", { diff --git a/terraform/module/policies.tf b/terraform/module/policies.tf index 0f69fff6..593c4935 100644 --- a/terraform/module/policies.tf +++ b/terraform/module/policies.tf @@ -34,6 +34,19 @@ resource "aws_iam_policy_attachment" "initializer-attach" { policy_arn = aws_iam_policy.initializer_policy.arn } +resource "aws_iam_policy" "publisher_policy" { + name = "${var.lambda_function_prefix}_publisher-policy" + description = "Lambda power tuning policy - Publisher - Terraform" + + policy = local.publisher_template +} + +resource "aws_iam_policy_attachment" "publisher-attach" { + name = "publisher-attachment" + roles = [aws_iam_role.publisher_role.name] + policy_arn = aws_iam_policy.publisher_policy.arn +} + resource "aws_iam_policy" "cleaner_policy" { name = "${var.lambda_function_prefix}_cleaner-policy" description = "Lambda power tuning policy - Cleaner - Terraform" diff --git a/terraform/module/roles.tf b/terraform/module/roles.tf index b6ac35b7..f5586838 100644 --- a/terraform/module/roles.tf +++ b/terraform/module/roles.tf @@ -26,6 +26,13 @@ resource "aws_iam_role" "initializer_role" { assume_role_policy = file("${path.module}/json_files/lambda.json") } +resource "aws_iam_role" "publisher_role" { + name = "${var.lambda_function_prefix}-publisher_role" + permissions_boundary = var.permissions_boundary + path = local.role_path + assume_role_policy = file("${path.module}/json_files/lambda.json") +} + resource "aws_iam_role" "cleaner_role" { name = "${var.lambda_function_prefix}-cleaner_role" permissions_boundary = var.permissions_boundary diff --git a/test/unit/test-lambda.js b/test/unit/test-lambda.js index 193bc585..9d29f268 100644 --- a/test/unit/test-lambda.js +++ b/test/unit/test-lambda.js @@ -33,7 +33,6 @@ const fakeContext = {}; // variables used during tests var setLambdaPowerCounter, - getLambdaPowerCounter, publishLambdaVersionCounter, createLambdaAliasCounter, updateLambdaAliasCounter, @@ -91,11 +90,11 @@ var getLambdaAliasStub, /** unit tests below **/ +const singleAliasConfig = { aliases: ['RAM128']}; describe('Lambda Functions', async() => { beforeEach('mock utilities', () => { setLambdaPowerCounter = 0; - getLambdaPowerCounter = 0; publishLambdaVersionCounter = 0; createLambdaAliasCounter = 0; updateLambdaAliasCounter = 0; @@ -116,11 +115,6 @@ describe('Lambda Functions', async() => { const error = new ResourceNotFoundException('alias is not defined'); throw error; }); - sandBox.stub(utils, 'getLambdaPower') - .callsFake(async() => { - getLambdaPowerCounter++; - return 1024; - }); setLambdaPowerStub = sandBox.stub(utils, 'setLambdaPower') .callsFake(async() => { setLambdaPowerCounter++; @@ -211,19 +205,159 @@ describe('Lambda Functions', async() => { it('should invoke the given cb with powerValues=ALL as input', async() => { const generatedValues = await invokeForSuccess(handler, { lambdaARN: 'arnOK', num: 5, powerValues: 'ALL' }); - expect(generatedValues.length).to.be(46); + expect(generatedValues.initConfigurations.length).to.be(47); // 46 power values plus the previous Lambda power configuration }); - it('should create N aliases and versions', async() => { - await invokeForSuccess(handler, { lambdaARN: 'arnOK', num: 5 }); + it('should generate N configurations', async() => { + const generatedValues = await invokeForSuccess(handler, { lambdaARN: 'arnOK', num: 5 }); + + // +1 because it will also reset power to its initial value + expect(generatedValues.initConfigurations.length).to.be(powerValues.length + 1); + }); + it('should generate N configurations', async() => { + const generatedValues = await invokeForSuccess(handler, { lambdaARN: 'arnOK', num: 5 }); // +1 because it will also reset power to its initial value - expect(setLambdaPowerCounter).to.be(powerValues.length + 1); + expect(generatedValues.initConfigurations.length).to.be(powerValues.length + 1); + }); + it('should generate an alias for each `num` and `powerValue` when `onlyColdStarts` is set', async() => { + + const generatedValues = await invokeForSuccess(handler, { lambdaARN: 'arnOK', num: 5, onlyColdStarts: true}); + + // +1 because it will also reset power to its initial value + expect(generatedValues.initConfigurations.length).to.be((powerValues.length * 5) + 1); + }); + + }); + + describe('publisher', async() => { + + const handler = require('../../lambda/publisher').handler; + + const invalidEvents = [ + { }, + {lambdaARN: 'arnOK'}, + {lambdaARN: 'arnOK', lambdaConfigurations: {}}, + { + lambdaARN: 'arnOK', + lambdaConfigurations: { + initConfigurations: [{ + powerValue: 512, + alias: 'RAM512', + }], + }, + }, + { + lambdaARN: 'arnOK', + lambdaConfigurations: { + iterator: { + index: 1, + count: 1, + }, + }, + }, + { + lambdaARN: 'arnOK', + lambdaConfigurations: { + initConfigurations: [{ + powerValue: 512, + alias: 'RAM512', + }, { + powerValue: 1024, + alias: 'RAM1024', + }], + iterator: { + index: 2, + count: 3, + }, + }, + }, + { + lambdaARN: 'arnOK', + lambdaConfigurations: { + initConfigurations: [{ + powerValue: 512, + alias: 'RAM512', + }, { + powerValue: 1024, + alias: 'RAM1024', + }], + iterator: { + index: 3, + count: 2, + }, + }, + }, + ]; + + invalidEvents.forEach(async(event) => { + it('should explode if invoked with invalid payload - ' + JSON.stringify(event), async() => { + await invokeForFailure(handler, event); + }); + }); + + it('should publish the given lambda version (first iteration)', async() => { + const generatedValues = await invokeForSuccess(handler, { + lambdaARN: 'arnOK', + lambdaConfigurations: { + initConfigurations: [{ + powerValue: 512, + alias: 'RAM512', + }, { + powerValue: 1024, + alias: 'RAM1024', + }], + iterator: { + index: 0, + count: 2, + }, + }}); + expect(setLambdaPowerCounter).to.be(1); + expect(waitForFunctionUpdateCounter).to.be(1); + expect(publishLambdaVersionCounter).to.be(1); + expect(createLambdaAliasCounter).to.be(1); + expect(generatedValues.iterator.index).to.be(1); // index should be incremented by 1 + expect(generatedValues.iterator.continue).to.be(true); // the iterator should be set to continue=false + expect(generatedValues.initConfigurations).to.be.a('array'); // initConfigurations should be a list + }); + + it('should publish the given lambda version (last iteration)', async() => { + const generatedValues = await invokeForSuccess(handler, { + lambdaARN: 'arnOK', + lambdaConfigurations: { + initConfigurations: [{ + powerValue: 512, + alias: 'RAM512', + }, { + powerValue: 1024, + alias: 'RAM1024', + }], + iterator: { + index: 1, + count: 2, + }, + }}); + expect(setLambdaPowerCounter).to.be(1); + expect(waitForFunctionUpdateCounter).to.be(1); + expect(publishLambdaVersionCounter).to.be(1); + expect(createLambdaAliasCounter).to.be(1); + expect(generatedValues.iterator.index).to.be(2); // index should be incremented by 1 + expect(generatedValues.iterator.continue).to.be(false); // the iterator should be set to continue=false + expect(generatedValues.initConfigurations).to.be(undefined); // initConfigurations should be unset + }); - expect(getLambdaPowerCounter).to.be(1); - expect(publishLambdaVersionCounter).to.be(powerValues.length); - expect(createLambdaAliasCounter).to.be(powerValues.length); - expect(waitForFunctionUpdateCounter).to.be(powerValues.length); + it('should publish the version even if an alias is not specified', async() => { + await invokeForSuccess(handler, { + lambdaARN: 'arnOK', + lambdaConfigurations: { + initConfigurations: [{ + powerValue: 512, + }], + iterator: { + index: 0, + count: 1, + }, + }}); }); it('should update an alias if it already exists', async() => { @@ -237,22 +371,21 @@ describe('Lambda Functions', async() => { throw error; } }); - await invokeForSuccess(handler, { lambdaARN: 'arnOK', num: 5 }); + await invokeForSuccess(handler, { + lambdaARN: 'arnOK', + lambdaConfigurations: { + initConfigurations: [{ + powerValue: 128, + alias: 'RAM128', + }], + iterator: { + index: 0, + count: 1, + }, + }}); expect(updateLambdaAliasCounter).to.be(1); - expect(createLambdaAliasCounter).to.be(powerValues.length - 1); - expect(waitForFunctionUpdateCounter).to.be(powerValues.length); - }); - - it('should update an alias if it already exists (2)', async() => { - createLambdaAliasStub && createLambdaAliasStub.restore(); - createLambdaAliasStub = sandBox.stub(utils, 'createLambdaAlias') - .callsFake(async() => { - createLambdaAliasCounter += 10; - throw new Error('Alias already exists'); - }); - await invokeForSuccess(handler, { lambdaARN: 'arnOK', num: 5 }); - expect(createLambdaAliasCounter).to.be(powerValues.length * 10); - expect(waitForFunctionUpdateCounter).to.be(powerValues.length); + expect(createLambdaAliasCounter).to.be(0); + expect(waitForFunctionUpdateCounter).to.be(1); }); it('should explode if something goes wrong during power set', async() => { @@ -261,10 +394,41 @@ describe('Lambda Functions', async() => { .callsFake(async() => { throw new Error('Something went wrong'); }); - await invokeForFailure(handler, { lambdaARN: 'arnOK', num: 5 }); + await invokeForFailure(handler, { + lambdaARN: 'arnOK', + lambdaConfigurations: { + initConfigurations: [{ + powerValue: 128, + alias: 'RAM128', + }], + iterator: { + index: 0, + count: 1, + }, + }}); expect(waitForFunctionUpdateCounter).to.be(0); }); + it('should NOT explode if something goes wrong during alias creation but it already exists', async() => { + createLambdaAliasStub && createLambdaAliasStub.restore(); + createLambdaAliasStub = sandBox.stub(utils, 'createLambdaAlias') + .callsFake(async() => { + throw new Error('Alias already exists'); + }); + await invokeForSuccess(handler, { + lambdaARN: 'arnOK', + lambdaConfigurations: { + initConfigurations: [{ + powerValue: 128, + alias: 'RAM128', + }], + iterator: { + index: 0, + count: 1, + }, + }}); + }); + it('should fail is something goes wrong with the initialization API calls', async() => { getLambdaAliasStub && getLambdaAliasStub.restore(); getLambdaAliasStub = sandBox.stub(utils, 'getLambdaAlias') @@ -272,10 +436,20 @@ describe('Lambda Functions', async() => { const error = new Error('very bad error'); throw error; }); - await invokeForFailure(handler, { lambdaARN: 'arnOK', num: 5 }); + await invokeForFailure(handler, { + lambdaARN: 'arnOK', + lambdaConfigurations: { + initConfigurations: [{ + powerValue: 128, + alias: 'RAM128', + }], + iterator: { + index: 0, + count: 1, + }, + }}); expect(waitForFunctionUpdateCounter).to.be(1); }); - }); describe('cleaner', async() => { @@ -285,10 +459,11 @@ describe('Lambda Functions', async() => { let invalidEvents = [ null, {}, - { lambdaARN: null }, - { lambdaARN: '' }, - { lambdaARN: false }, - { lambdaARN: 0 }, + { lambdaARN: null, lambdaConfigurations: singleAliasConfig}, + { lambdaARN: '', lambdaConfigurations: singleAliasConfig}, + { lambdaARN: false, lambdaConfigurations: singleAliasConfig}, + { lambdaARN: 0, lambdaConfigurations: singleAliasConfig}, + { lambdaARN: '', lambdaConfigurations: singleAliasConfig}, ]; invalidEvents.forEach(async(event) => { @@ -297,7 +472,19 @@ describe('Lambda Functions', async() => { }); }); - it('should explode if invoked without powerValues', async() => { + invalidEvents = [ + { lambdaARN: 'arnOK'}, + { lambdaARN: 'arnOK', lambdaConfigurations: {}}, + { lambdaARN: 'arnOK', lambdaConfigurations: { powerValues: []}}, + ]; + + invalidEvents.forEach(async(event) => { + it('should explode if invoked without valid powerValues - ' + JSON.stringify(event), async() => { + await invokeForFailure(handler, event); + }); + }); + + it('should explode if invoked without lambdaConfigurations', async() => { await invokeForFailure(handler, {lambdaARN: 'arnOK'}); }); @@ -319,7 +506,11 @@ describe('Lambda Functions', async() => { }); }); - const eventOK = { lambdaARN: 'arnOK', powerValues: ['128', '256', '512'] }; + const eventOK = { + num: 10, + lambdaARN: 'arnOK', + lambdaConfigurations: {powerValues: ['128', '256', '512'] }, + }; it('should invoke the given cb, when done', async() => { await invokeForSuccess(handler, eventOK); @@ -355,6 +546,51 @@ describe('Lambda Functions', async() => { await invokeForFailure(handler, eventOK); }); + it('should work fine even with onlyColdStarts=true', async() => { + await invokeForSuccess(handler, { + num: 10, + lambdaARN: 'arnOK', + lambdaConfigurations: {powerValues: ['128', '256', '512'] }, + onlyColdStarts: true, + }); + }); + + it('should clean the right aliases with onlyColdStarts=false', async() => { + const cleanedAliases = []; + const expectedAliases = ['RAM128', 'RAM256', 'RAM512']; + deleteLambdaAliasStub && deleteLambdaAliasStub.restore(); + deleteLambdaAliasStub = sandBox.stub(utils, 'deleteLambdaAlias') + .callsFake(async(lambdaARN, alias) => { + cleanedAliases.push(alias); + return 'OK'; + }); + await invokeForSuccess(handler, { + num: 10, + lambdaARN: 'arnOK', + lambdaConfigurations: {powerValues: ['128', '256', '512']}, + onlyColdStarts: false, + }); + expect(cleanedAliases).to.eql(expectedAliases); + }); + + it('should clean the right aliases with onlyColdStarts=true', async() => { + const cleanedAliases = []; + const expectedAliases = ['RAM128-0', 'RAM128-1', 'RAM256-0', 'RAM256-1', 'RAM512-0', 'RAM512-1']; + deleteLambdaAliasStub && deleteLambdaAliasStub.restore(); + deleteLambdaAliasStub = sandBox.stub(utils, 'deleteLambdaAlias') + .callsFake(async(lambdaARN, alias) => { + cleanedAliases.push(alias); + return 'OK'; + }); + await invokeForSuccess(handler, { + num: 2, + lambdaARN: 'arnOK', + lambdaConfigurations: {powerValues: ['128', '256', '512']}, + onlyColdStarts: true, + }); + expect(cleanedAliases).to.eql(expectedAliases); + }); + }); describe('executor', () => { @@ -1302,76 +1538,76 @@ describe('Lambda Functions', async() => { const trimmedDurationsValues = [ 3.5, 3.5, - 4.333333333333333, - 27.7, + 4.416666666666667, + 27.72, + ]; + + const logResults = [ + // Duration 0.1ms - Init Duration 0.1ms - Billed 1ms + { + StatusCode: 200, + LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMC4xIG1zCUJpbGxlZCBEdXJhdGlvbjogMSBtcwlJbml0IER1cmF0aW9uOiAwLjEgbXMgCU1lbW9yeSBTaXplOiAxMjggTUIJTWF4IE1lbW9yeSBVc2VkOiAxNSBNQgkK', + Payload: 'null', + }, + // Duration 0.5ms - Billed 1ms + { + StatusCode: 200, + LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMC41IG1zCUJpbGxlZCBEdXJhdGlvbjogMSBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCQo=', + Payload: 'null', + }, + // Duration 2.0ms - Billed 2ms + { + StatusCode: 200, + LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkgMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQgMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQgMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQgRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkIFJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMi4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMiBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCQ==', + Payload: 'null', + }, + // Duration 3.0ms - Billed 3ms + { + StatusCode: 200, + LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMy4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMyBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCQo=', + Payload: 'null', + }, + // Duration 3.0ms - Billed 3ms + { + StatusCode: 200, + LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMy4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMyBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCQo=', + Payload: 'null', + }, + // Duration 4.0ms - Billed 4ms + { + StatusCode: 200, + LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogNC4wIG1zCUJpbGxlZCBEdXJhdGlvbjogNCBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCQo=', + Payload: 'null', + }, + // Duration 4.5ms - Billed 5ms + { + StatusCode: 200, + LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogNC41IG1zCUJpbGxlZCBEdXJhdGlvbjogNSBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCQo=', + Payload: 'null', + }, + // Duration 10.0ms - Billed 10ms + { + StatusCode: 200, + LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMTAuMCBtcwlCaWxsZWQgRHVyYXRpb246IDEwIG1zIAlNZW1vcnkgU2l6ZTogMTI4IE1CCU1heCBNZW1vcnkgVXNlZDogMTUgTUIJCg==', + Payload: 'null', + }, + // Duration 50ms - Billed 50ms + { + StatusCode: 200, + LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogNTAuMCBtcwlCaWxsZWQgRHVyYXRpb246IDUwIG1zIAlNZW1vcnkgU2l6ZTogMTI4IE1CCU1heCBNZW1vcnkgVXNlZDogMTUgTUIJCg==', + Payload: 'null', + }, + // Duration 200ms - Billed 200ms + { + StatusCode: 200, + LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMjAwLjAgbXMJQmlsbGVkIER1cmF0aW9uOiAyMDAgbXMgCU1lbW9yeSBTaXplOiAxMjggTUIJTWF4IE1lbW9yeSBVc2VkOiAxNSBNQgkK', + Payload: 'null', + }, ]; discardTopBottomValues.forEach((discardTopBottomValue, forEachIndex) => { console.log('extractDiscardTopBottomValue', discardTopBottomValue); it(`should discard ${discardTopBottomValue * 100}% of durations`, async() => { - const logResults = [ - // Duration 0.1ms - Billed 1ms - { - StatusCode: 200, - LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMC4xIG1zCUJpbGxlZCBEdXJhdGlvbjogMSBtcwlJbml0IER1cmF0aW9uOiAwLjEgbXMgCU1lbW9yeSBTaXplOiAxMjggTUIJTWF4IE1lbW9yeSBVc2VkOiAxNSBNQgkK', - Payload: 'null', - }, - // Duration 0.5ms - Billed 1ms - { - StatusCode: 200, - LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMC41IG1zCUJpbGxlZCBEdXJhdGlvbjogMSBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCQo=', - Payload: 'null', - }, - // Duration 2.0ms - Billed 2ms - { - StatusCode: 200, - LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMi4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMm1zIAlNZW1vcnkgU2l6ZTogMTI4IE1CCU1heCBNZW1vcnkgVXNlZDogMTUgTUIJ', - Payload: 'null', - }, - // Duration 3.0ms - Billed 3ms - { - StatusCode: 200, - LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMy4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMyBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCQo=', - Payload: 'null', - }, - // Duration 3.0ms - Billed 3ms - { - StatusCode: 200, - LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMy4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMyBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCQo=', - Payload: 'null', - }, - // Duration 4.0ms - Billed 4ms - { - StatusCode: 200, - LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogNC4wIG1zCUJpbGxlZCBEdXJhdGlvbjogNCBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCQo=', - Payload: 'null', - }, - // Duration 4.5ms - Billed 5ms - { - StatusCode: 200, - LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogNC41IG1zCUJpbGxlZCBEdXJhdGlvbjogNSBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCQo=', - Payload: 'null', - }, - // Duration 10.0ms - Billed 10ms - { - StatusCode: 200, - LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMTAuMCBtcwlCaWxsZWQgRHVyYXRpb246IDEwIG1zIAlNZW1vcnkgU2l6ZTogMTI4IE1CCU1heCBNZW1vcnkgVXNlZDogMTUgTUIJCg==', - Payload: 'null', - }, - // Duration 50ms - Billed 50ms - { - StatusCode: 200, - LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogNTAuMCBtcwlCaWxsZWQgRHVyYXRpb246IDUwIG1zIAlNZW1vcnkgU2l6ZTogMTI4IE1CCU1heCBNZW1vcnkgVXNlZDogMTUgTUIJCg==', - Payload: 'null', - }, - // Duration 200ms - Billed 200ms - { - StatusCode: 200, - LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMjAwLjAgbXMJQmlsbGVkIER1cmF0aW9uOiAyMDAgbXMgCU1lbW9yeSBTaXplOiAxMjggTUIJTWF4IE1lbW9yeSBVc2VkOiAxNSBNQgkK', - Payload: 'null', - }, - ]; - let invokeCounter = 0; invokeLambdaStub && invokeLambdaStub.restore(); invokeLambdaStub = sandBox.stub(utils, 'invokeLambda') @@ -1397,6 +1633,72 @@ describe('Lambda Functions', async() => { expect(response.averageDuration).to.be(trimmedDurationsValues[forEachIndex]); }); }); + + it('should default discardTopBottom to 0 when onlyColdStarts', async() => { + let invokeCounter = 0; + invokeLambdaStub && invokeLambdaStub.restore(); + invokeLambdaStub = sandBox.stub(utils, 'invokeLambda') + .callsFake(async(_arn, _alias, payload) => { + invokeLambdaPayloads.push(payload); + const logResult = logResults[invokeCounter]; + invokeCounter++; + + return logResult; + }); + + const response = await invokeForSuccess(handler, { + value: '128', + input: { + lambdaARN: 'arnOK', + num: 10, + onlyColdStarts: true, + }, + }); + + console.log('response', response); + + expect(response.averageDuration).to.be(27.72); + }); + + it('should waitForAliasActive for each Alias when onlyColdStarts is set', async() => { + await invokeForSuccess(handler, { + value: '128', + input: { + lambdaARN: 'arnOK', + num: 10, + onlyColdStarts: true, + parallelInvocation: true, + }, + }); + expect(waitForAliasActiveCounter).to.be(10); + }); + + it('should invoke each Alias once when onlyColdStarts is set', async() => { + const aliasesToInvoke = ['RAM128-0', 'RAM128-1', 'RAM128-2', 'RAM128-3', 'RAM128-4']; + let invokedAliases = []; + let invokeCounter = 0; + invokeLambdaStub && invokeLambdaStub.restore(); + invokeLambdaStub = sandBox.stub(utils, 'invokeLambda') + .callsFake(async(_arn, _alias, payload) => { + invokedAliases.push(_alias); + invokeLambdaPayloads.push(payload); + const logResult = logResults[invokeCounter]; + invokeCounter++; + + return logResult; + }); + await invokeForSuccess(handler, { + value: '128', + input: { + lambdaARN: 'arnOK', + num: 5, + onlyColdStarts: true, + parallelInvocation: true, + }, + }); + expect(waitForAliasActiveCounter).to.be(5); + expect(invokedAliases).to.eql(aliasesToInvoke); + }); }); describe('analyzer', () => { diff --git a/test/unit/test-utils.js b/test/unit/test-utils.js index cf8517a6..f70c19a9 100644 --- a/test/unit/test-utils.js +++ b/test/unit/test-utils.js @@ -33,7 +33,13 @@ const sandBox = sinon.createSandbox(); const lambdaMock = awsV3Mock.mockClient(LambdaClient); lambdaMock.reset(); lambdaMock.on(GetAliasCommand).resolves({}); -lambdaMock.on(GetFunctionConfigurationCommand).resolves({ MemorySize: 1024, State: 'Active', LastUpdateStatus: 'Successful', Architectures: ['x86_64'] }); +lambdaMock.on(GetFunctionConfigurationCommand).resolves({ + MemorySize: 1024, + State: 'Active', + LastUpdateStatus: 'Successful', + Architectures: ['x86_64'], + Description: 'Sample Description', +}); lambdaMock.on(UpdateFunctionConfigurationCommand).resolves({}); lambdaMock.on(PublishVersionCommand).resolves({}); lambdaMock.on(DeleteFunctionCommand).resolves({}); @@ -101,7 +107,7 @@ describe('Lambda Utils', () => { sandBox.restore(); }); - describe('stepFunctionsCost', () => { + describe('stepFunctionsBaseCost', () => { it('should return expected step base cost', () => { process.env.sfCosts = '{"us-gov-west-1": 0.00003, "default": 0.000025}'; process.env.AWS_REGION = 'us-gov-west-1'; @@ -116,21 +122,51 @@ describe('Lambda Utils', () => { }); }); - describe('stepFunctionsBaseCost', () => { + describe('stepFunctionsCost', () => { it('should return expected step total cost', () => { process.env.sfCosts = '{"us-gov-west-1": 0.00003, "default": 0.000025}'; process.env.AWS_REGION = 'us-gov-west-1'; const nPower = 10; - const expectedCost = +(0.00003 * (6 + nPower)).toFixed(5); - const result = utils.stepFunctionsCost(nPower); + const expectedCost = 0.00108; + const result = utils.stepFunctionsCost(nPower, false, 10); + expect(result).to.be.equal(expectedCost); + }); + it('should return expected step total cost when onlyColdStarts=true', () => { + process.env.sfCosts = '{"us-gov-west-1": 0.00003, "default": 0.000025}'; + process.env.AWS_REGION = 'us-gov-west-1'; + const nPower = 10; + const expectedCost = 0.00648; + const result = utils.stepFunctionsCost(nPower, true, 10); expect(result).to.be.equal(expectedCost); }); }); describe('getLambdaPower', () => { - it('should return the memory value', async() => { + it('should return the power value and description', async() => { + lambdaMock.on(GetFunctionConfigurationCommand).resolves({ + MemorySize: 1024, + State: 'Active', + LastUpdateStatus: 'Successful', + Architectures: ['x86_64'], + Description: 'Sample Description', // this is null if no vars are set + }); + const value = await utils.getLambdaPower('arn:aws:lambda:us-east-1:XXX:function:YYY'); + expect(value.power).to.be(1024); + expect(value.description).to.be('Sample Description'); + }); + + it('should return the power value and description, even if empty', async() => { + lambdaMock.on(GetFunctionConfigurationCommand).resolves({ + MemorySize: 1024, + State: 'Active', + LastUpdateStatus: 'Successful', + Architectures: ['x86_64'], + Description: '', // this is null if no vars are set + }); + const value = await utils.getLambdaPower('arn:aws:lambda:us-east-1:XXX:function:YYY'); - expect(value).to.be(1024); + expect(value.power).to.be(1024); + expect(value.description).to.be(''); }); }); @@ -174,39 +210,69 @@ describe('Lambda Utils', () => { }); + const textLog = + 'START RequestId: 55bc566d-1e2c-11e7-93e6-6705ceb4c1cc Version: $LATEST\n' + + 'END RequestId: 55bc566d-1e2c-11e7-93e6-6705ceb4c1cc\n' + + 'REPORT RequestId: 55bc566d-1e2c-11e7-93e6-6705ceb4c1cc\tDuration: 469.40 ms\tBilled Duration: 500 ms\tMemory Size: 1024 MB\tMax Memory Used: 21 MB\tInit Duration: 100.99 ms' + ; + const textLogSnapStart = + 'START RequestId: 55bc566d-1e2c-11e7-93e6-6705ceb4c1cc Version: $LATEST\n' + + 'END RequestId: 55bc566d-1e2c-11e7-93e6-6705ceb4c1cc\n' + + 'REPORT RequestId: 55bc566d-1e2c-11e7-93e6-6705ceb4c1cc\tDuration: 469.40 ms\tBilled Duration: 500 ms\tMemory Size: 1024 MB\tMax Memory Used: 21 MB\tRestore Duration: 474.16 ms\tBilled Restore Duration: 75 ms' + ; + + // JSON logs contain multiple objects, seperated by a newline + const jsonLog = + '{"timestamp":"2024-02-09T08:42:44.078Z","level":"INFO","requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","message":"Just some logs here =)"}\n' + + '{"time":"2024-02-09T08:42:44.078Z","type":"platform.start","record":{"requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","version":"8"}}\n' + + '{"time":"2024-02-09T08:42:44.079Z","type":"platform.runtimeDone","record":{"requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","status":"success","spans":[{"name":"responseLatency","start":"2024-02-09T08:42:44.078Z","durationMs":0.677},{"name":"responseDuration","start":"2024-02-09T08:42:44.079Z","durationMs":0.035},{"name":"runtimeOverhead","start":"2024-02-09T08:42:44.079Z","durationMs":0.211}],"metrics":{"durationMs":1.056,"producedBytes":50}}}\n' + + '{"time":"2024-02-09T08:42:44.080Z","type":"platform.report","record":{"requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","status":"success","metrics":{"durationMs":1.317,"billedDurationMs":2,"memorySizeMB":1024,"maxMemoryUsedMB":68,"initDurationMs": 10}}}' + ; + + const jsonLogSnapStart = + '{"timestamp":"2024-02-09T08:42:44.078Z","level":"INFO","requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","message":"Just some logs here =)"}\n' + + '{"time":"2024-02-09T08:42:44.078Z","type":"platform.start","record":{"requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","version":"8"}}\n' + + '{"time":"2024-02-09T08:42:44.079Z","type":"platform.runtimeDone","record":{"requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","status":"success","spans":[{"name":"responseLatency","start":"2024-02-09T08:42:44.078Z","durationMs":0.677},{"name":"responseDuration","start":"2024-02-09T08:42:44.079Z","durationMs":0.035},{"name":"runtimeOverhead","start":"2024-02-09T08:42:44.079Z","durationMs":0.211}],"metrics":{"durationMs":1.056,"producedBytes":50}}}\n' + + '{"time":"2024-02-09T08:42:44.080Z","type":"platform.report","record":{"requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","status":"success","metrics":{"durationMs": 147.156,"billedDurationMs": 201, "memorySizeMB": 512,"maxMemoryUsedMB": 91,"restoreDurationMs": 500.795,"billedRestoreDurationMs": 53 }}}' + ; + + const jsonMixedLog = + '{"timestamp":"2024-02-09T08:42:44.078Z","level":"INFO","requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","message":"Just some logs here =)"}\n' + + '[AWS Parameters and Secrets Lambda Extension] 2024/04/11 02:14:17 PARAMETERS_SECRETS_EXTENSION_LOG_LEVEL is info. Log level set to info.' + + '{"time":"2024-02-09T08:42:44.078Z","type":"platform.start","record":{"requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","version":"8"}}\n' + + '{"time":"2024-02-09T08:42:44.079Z","type":"platform.runtimeDone","record":{"requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","status":"success","spans":[{"name":"responseLatency","start":"2024-02-09T08:42:44.078Z","durationMs":0.677},{"name":"responseDuration","start":"2024-02-09T08:42:44.079Z","durationMs":0.035},{"name":"runtimeOverhead","start":"2024-02-09T08:42:44.079Z","durationMs":0.211}],"metrics":{"durationMs":1.056,"producedBytes":50}}}\n' + + '{"time":"2024-02-09T08:42:44.080Z","type":"platform.report","record":{"requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","status":"success","metrics":{"durationMs":1.317,"billedDurationMs":4,"memorySizeMB":1024,"maxMemoryUsedMB":68,"initDurationMs": 20}}}' + ; + + const jsonMixedLogWithInvalidJSON = + '{"timestamp":"2024-02-09T08:42:44.078Z","level":"INFO","requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","message":"Just some logs here =)"\n' + // missing } here + '[AWS Parameters and Secrets Lambda Extension] 2024/04/11 02:14:17 PARAMETERS_SECRETS_EXTENSION_LOG_LEVEL is info. Log level set to info.' + + '{"time":"2024-02-09T08:42:44.078Z","type":"platform.start","record":{"requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","version":"8"}}\n' + + '{"time":"2024-02-09T08:42:44.079Z","type":"platform.runtimeDone","record":{"requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","status":"success","spans":[{"name":"responseLatency","start":"2024-02-09T08:42:44.078Z","durationMs":0.677},{"name":"responseDuration","start":"2024-02-09T08:42:44.079Z","durationMs":0.035},{"name":"runtimeOverhead","start":"2024-02-09T08:42:44.079Z","durationMs":0.211}],"metrics":{"durationMs":1.056,"producedBytes":50}}}\n' + + '{"time":"2024-02-09T08:42:44.080Z","type":"platform.report","record":{"requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","status":"success","metrics":{"durationMs":1.317,"billedDurationMs":8,"memorySizeMB":1024,"maxMemoryUsedMB":68,"initDurationMs": 30}}}' + ; + + const invalidJSONLog = '{"timestamp":"2024-02-09T08:42:44.078Z","level":"INFO","requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","message":"Just some logs here =)"}'; + describe('extractDuration', () => { - const textLog = - 'START RequestId: 55bc566d-1e2c-11e7-93e6-6705ceb4c1cc Version: $LATEST\n' + - 'END RequestId: 55bc566d-1e2c-11e7-93e6-6705ceb4c1cc\n' + - 'REPORT RequestId: 55bc566d-1e2c-11e7-93e6-6705ceb4c1cc\tDuration: 469.40 ms\tBilled Duration: 500 ms\tMemory Size: 1024 MB\tMax Memory Used: 21 MB' - ; - - // JSON logs contain multiple objects, seperated by a newline - const jsonLog = - '{"timestamp":"2024-02-09T08:42:44.078Z","level":"INFO","requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","message":"Just some logs here =)"}\n' + - '{"time":"2024-02-09T08:42:44.078Z","type":"platform.start","record":{"requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","version":"8"}}\n' + - '{"time":"2024-02-09T08:42:44.079Z","type":"platform.runtimeDone","record":{"requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","status":"success","spans":[{"name":"responseLatency","start":"2024-02-09T08:42:44.078Z","durationMs":0.677},{"name":"responseDuration","start":"2024-02-09T08:42:44.079Z","durationMs":0.035},{"name":"runtimeOverhead","start":"2024-02-09T08:42:44.079Z","durationMs":0.211}],"metrics":{"durationMs":1.056,"producedBytes":50}}}\n' + - '{"time":"2024-02-09T08:42:44.080Z","type":"platform.report","record":{"requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","status":"success","metrics":{"durationMs":1.317,"billedDurationMs":2,"memorySizeMB":1024,"maxMemoryUsedMB":68}}}' - ; - - const jsonMixedLog = - '{"timestamp":"2024-02-09T08:42:44.078Z","level":"INFO","requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","message":"Just some logs here =)"}\n' + - '[AWS Parameters and Secrets Lambda Extension] 2024/04/11 02:14:17 PARAMETERS_SECRETS_EXTENSION_LOG_LEVEL is info. Log level set to info.' + - '{"time":"2024-02-09T08:42:44.078Z","type":"platform.start","record":{"requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","version":"8"}}\n' + - '{"time":"2024-02-09T08:42:44.079Z","type":"platform.runtimeDone","record":{"requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","status":"success","spans":[{"name":"responseLatency","start":"2024-02-09T08:42:44.078Z","durationMs":0.677},{"name":"responseDuration","start":"2024-02-09T08:42:44.079Z","durationMs":0.035},{"name":"runtimeOverhead","start":"2024-02-09T08:42:44.079Z","durationMs":0.211}],"metrics":{"durationMs":1.056,"producedBytes":50}}}\n' + - '{"time":"2024-02-09T08:42:44.080Z","type":"platform.report","record":{"requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","status":"success","metrics":{"durationMs":1.317,"billedDurationMs":4,"memorySizeMB":1024,"maxMemoryUsedMB":68}}}' - ; - - const jsonMixedLogWithInvalidJSON = - '{"timestamp":"2024-02-09T08:42:44.078Z","level":"INFO","requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","message":"Just some logs here =)"\n' + // missing } here - '[AWS Parameters and Secrets Lambda Extension] 2024/04/11 02:14:17 PARAMETERS_SECRETS_EXTENSION_LOG_LEVEL is info. Log level set to info.' + - '{"time":"2024-02-09T08:42:44.078Z","type":"platform.start","record":{"requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","version":"8"}}\n' + - '{"time":"2024-02-09T08:42:44.079Z","type":"platform.runtimeDone","record":{"requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","status":"success","spans":[{"name":"responseLatency","start":"2024-02-09T08:42:44.078Z","durationMs":0.677},{"name":"responseDuration","start":"2024-02-09T08:42:44.079Z","durationMs":0.035},{"name":"runtimeOverhead","start":"2024-02-09T08:42:44.079Z","durationMs":0.211}],"metrics":{"durationMs":1.056,"producedBytes":50}}}\n' + - '{"time":"2024-02-09T08:42:44.080Z","type":"platform.report","record":{"requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","status":"success","metrics":{"durationMs":1.317,"billedDurationMs":8,"memorySizeMB":1024,"maxMemoryUsedMB":68}}}' - ; it('should extract the duration from a Lambda log (text format)', () => { - expect(utils.extractDuration(textLog)).to.be(500); + expect(utils.extractDuration(textLog)).to.be(469.4); + }); + + it('should retrieve the Init Duration from a Lambda log (text format)', () => { + expect(utils.extractDuration(textLog, utils.DURATIONS.initDurationMs)).to.be(100.99); + }); + + it('should retrieve the Billed Duration from a Lambda log (text format)', () => { + expect(utils.extractDuration(textLog, utils.DURATIONS.billedDurationMs)).to.be(500); + }); + + it('should retrieve the Restore Duration from a SnapStart Lambda log (text format)', () => { + expect(utils.extractDuration(textLogSnapStart, utils.DURATIONS.restoreDurationMs)).to.be(474.16); + }); + it('should retrieve the Billed Restore Duration from a SnapStart Lambda log (text format)', () => { + expect(utils.extractDuration(textLogSnapStart, utils.DURATIONS.billedRestoreDurationMs)).to.be(75); }); it('should return 0 if duration is not found', () => { @@ -215,20 +281,55 @@ describe('Lambda Utils', () => { expect(utils.extractDuration(partialLog)).to.be(0); }); + it('should return 0 if Init Duration is not found', () => { + expect(utils.extractDuration('hello world', utils.DURATIONS.initDurationMs)).to.be(0); + const partialLog = 'START RequestId: 55bc566d-1e2c-11e7-93e6-6705ceb4c1cc Version: $LATEST\n'; + expect(utils.extractDuration(partialLog, utils.DURATIONS.initDurationMs)).to.be(0); + }); + + it('should return 0 if Restore Duration is not found', () => { + expect(utils.extractDuration('hello world', utils.DURATIONS.restoreDurationMs)).to.be(0); + const partialLog = 'START RequestId: 55bc566d-1e2c-11e7-93e6-6705ceb4c1cc Version: $LATEST\n'; + expect(utils.extractDuration(partialLog, utils.DURATIONS.restoreDurationMs)).to.be(0); + }); + + it('should return 0 if Billed Duration is not found', () => { + expect(utils.extractDuration('hello world', utils.DURATIONS.billedDurationMs)).to.be(0); + const partialLog = 'START RequestId: 55bc566d-1e2c-11e7-93e6-6705ceb4c1cc Version: $LATEST\n'; + expect(utils.extractDuration(partialLog, utils.DURATIONS.billedDurationMs)).to.be(0); + }); + + it('should return 0 if Billed Restore Duration is not found', () => { + expect(utils.extractDuration('hello world', utils.DURATIONS.billedRestoreDurationMs)).to.be(0); + const partialLog = 'START RequestId: 55bc566d-1e2c-11e7-93e6-6705ceb4c1cc Version: $LATEST\n'; + expect(utils.extractDuration(partialLog, utils.DURATIONS.billedRestoreDurationMs)).to.be(0); + }); + it('should extract the duration from a Lambda log (json format)', () => { - expect(utils.extractDuration(jsonLog)).to.be(2); + expect(utils.extractDuration(jsonLog, utils.DURATIONS.durationMs)).to.be(1.317); + }); + + it('should extract the Init duration from a Lambda log (json format)', () => { + expect(utils.extractDuration(jsonLog, utils.DURATIONS.initDurationMs)).to.be(10); + }); + + it('should extract the Restore duration from a Lambda log (json format)', () => { + expect(utils.extractDuration(jsonLogSnapStart, utils.DURATIONS.restoreDurationMs)).to.be(500.795); + }); + + it('should extract the Billed Restore duration from a Lambda log (json format)', () => { + expect(utils.extractDuration(jsonLogSnapStart, utils.DURATIONS.billedRestoreDurationMs)).to.be(53); }); it('should extract the duration from a Lambda log (json text mixed format)', () => { - expect(utils.extractDuration(jsonMixedLog)).to.be(4); + expect(utils.extractDuration(jsonMixedLog)).to.be(1.317); }); it('should extract the duration from a Lambda log (json text mixed format with invalid JSON)', () => { - expect(utils.extractDuration(jsonMixedLogWithInvalidJSON)).to.be(8); + expect(utils.extractDuration(jsonMixedLogWithInvalidJSON)).to.be(1.317); }); it('should explode if invalid json format document is provided', () => { - const invalidJSONLog = '{"timestamp":"2024-02-09T08:42:44.078Z","level":"INFO","requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","message":"Just some logs here =)"}'; expect(() => utils.extractDuration(invalidJSONLog)).to.throwError(); }); @@ -249,15 +350,15 @@ describe('Lambda Utils', () => { describe('parseLogAndExtractDurations', () => { const results = [ - // 1s (will be discarded) + // Duration 1ms { StatusCode: 200, LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMS4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMSBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1C', Payload: 'null' }, - // 1s + // Duration 1ms { StatusCode: 200, LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMS4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMSBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1C', Payload: 'null' }, - // 2s -> avg! + // Duration 2ms { StatusCode: 200, LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMi4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMiBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1C', Payload: 'null' }, - // 3s + // Duration 3ms { StatusCode: 200, LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMy4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMyBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1C', Payload: 'null' }, - // 3s (will be discarded) + // Duration 3ms { StatusCode: 200, LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMy4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMyBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1C', Payload: 'null' }, ]; @@ -280,6 +381,80 @@ describe('Lambda Utils', () => { expect(durations).to.be.an('array'); expect(durations).to.eql([0]); }); + + it('should give duration as initDuration + duration', () => { + const resultWithInitDuration = + { + StatusCode: 200, + // Duration: 469.40 ms Init Duration: 100.99 ms + LogResult: Buffer.from(textLog).toString('base64'), + }; + const durations = utils.parseLogAndExtractDurations([resultWithInitDuration]); + expect(durations).to.be.a('array'); + expect(durations.length).to.be(1); + expect(durations).to.eql([570.39]); + }); + + it('should give duration as restoreDuration + duration (for SnapStart)', () => { + const resultWithInitDuration = + { + StatusCode: 200, + // Duration: 469.40 ms - Restore Duration: 474.16 ms + LogResult: Buffer.from(textLogSnapStart).toString('base64'), + }; + const durations = utils.parseLogAndExtractDurations([resultWithInitDuration]); + expect(durations).to.be.a('array'); + expect(durations.length).to.be(1); + expect(durations).to.eql([943.56]); + }); + }); + describe('parseLogAndExtractBilledDurations', () => { + const results = [ + // Billed Duration 1ms + { StatusCode: 200, LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMS4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMSBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1C', Payload: 'null' }, + // Billed Duration 1ms + { StatusCode: 200, LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMS4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMSBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1C', Payload: 'null' }, + // Billed Duration 2ms + { StatusCode: 200, LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMi4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMiBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1C', Payload: 'null' }, + // Billed Duration 3ms + { StatusCode: 200, LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMy4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMyBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1C', Payload: 'null' }, + // Billed Duration 3ms + { StatusCode: 200, LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMy4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMyBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1C', Payload: 'null' }, + ]; + + it('should return the list of billed durations', () => { + const durations = utils.parseLogAndExtractBilledDurations(results); + expect(durations).to.be.a('array'); + expect(durations.length).to.be(5); + expect(durations).to.eql([1, 1, 2, 3, 3]); + }); + it('should return empty list if empty results', () => { + const durations = utils.parseLogAndExtractBilledDurations([]); + expect(durations).to.be.an('array'); + expect(durations.length).to.be(0); + }); + + it('should not explode if missing logs', () => { + const durations = utils.parseLogAndExtractBilledDurations([ + { StatusCode: 200, Payload: 'null' }, + ]); + expect(durations).to.be.an('array'); + expect(durations).to.eql([0]); + }); + + + it('should give duration as billedDuration + restoreDuration (for SnapStart)', () => { + const resultWithInitDuration = + { + StatusCode: 200, + // Billed Duration: 500 ms Billed Restore Duration: 75 ms + LogResult: Buffer.from(textLogSnapStart).toString('base64'), + }; + const durations = utils.parseLogAndExtractBilledDurations([resultWithInitDuration]); + expect(durations).to.be.a('array'); + expect(durations.length).to.be(1); + expect(durations).to.eql([575]); + }); }); describe('computeAverageDuration', () => { @@ -1146,4 +1321,20 @@ describe('Lambda Utils', () => { isPayloadInConsoleLog: false, })); }); + + describe('buildAliasString', () => { + + it('should return baseAlias if onlyColdStarts=false', async() => { + const value = utils.buildAliasString('RAM128', false, 0); + expect(value).to.be('RAM128'); + }); + it('should only require baseAlias', async() => { + const value = utils.buildAliasString('RAM128'); + expect(value).to.be('RAM128'); + }); + it('should append index to baseAlias if onlyColdStarts=true', async() => { + const value = utils.buildAliasString('RAM128', true, 1); + expect(value).to.be('RAM128-1'); + }); + }); });