From efd6ad0115c49b99033965c854955c8fd269ff86 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 10 May 2023 18:38:53 +0200 Subject: [PATCH 01/38] Another implementation of onlyColdStarts --- lambda/executor.js | 13 ++++++-- lambda/initializer.js | 75 ++++++++++++++++++++++++++++++++++++++----- lambda/utils.js | 26 ++++++++++++--- template.yml | 43 +++++++++++++++++++++++-- 4 files changed, 140 insertions(+), 17 deletions(-) diff --git a/lambda/executor.js b/lambda/executor.js index e48a53c..a77ebaa 100644 --- a/lambda/executor.js +++ b/lambda/executor.js @@ -19,6 +19,7 @@ module.exports.handler = async(event, context) => { let { lambdaARN, value, + alias, num, enableParallel, payload, @@ -26,6 +27,7 @@ module.exports.handler = async(event, context) => { preProcessorARN, postProcessorARN, discardTopBottom, + onlyColdStarts, sleepBetweenRunsMs, } = await extractDataFromInput(event); @@ -37,7 +39,7 @@ module.exports.handler = async(event, context) => { num = 1; } - const lambdaAlias = 'RAM' + value; + const lambdaAlias = alias; let results; // fetch architecture from $LATEST @@ -101,7 +103,11 @@ const extractDiscardTopBottomValue = (event) => { // extract discardTopBottom used to trim values from average duration let discardTopBottom = event.discardTopBottom; if (typeof discardTopBottom === 'undefined') { - discardTopBottom = 0.2; + if (event.onlyColdStarts){ + discardTopBottom = 0; + } else { + discardTopBottom = 0.2; + } } // discardTopBottom must be between 0 and 0.4 return Math.min(Math.max(discardTopBottom, 0.0), 0.4); @@ -123,7 +129,8 @@ const extractDataFromInput = async(event) => { const discardTopBottom = extractDiscardTopBottomValue(input); const sleepBetweenRunsMs = extractSleepTime(input); return { - value: parseInt(event.value, 10), + alias: event.value, + value: event.value.match(/\d+/g)[0], lambdaARN: input.lambdaARN, num: parseInt(input.num, 10), enableParallel: !!input.parallelInvocation, diff --git a/lambda/initializer.js b/lambda/initializer.js index bfd6a3c..7aeaf0f 100644 --- a/lambda/initializer.js +++ b/lambda/initializer.js @@ -8,24 +8,83 @@ 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, envVars} = await utils.getLambdaPower(lambdaARN); + + let lambdaFunctionsToSet = []; // 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){ + lambdaFunctionsToSet.push({lambdaARN: lambdaARN, powerValue: powerValue, envVars: envVars, alias: baseAlias}); + } else { + for (let n of utils.range(num)){ + let alias = utils.buildAliasString(baseAlias, onlyColdStarts, n); + const currentEnvVars = { + LambdaPowerTuningForceColdStart: alias, + ...envVars, + }; + // here we inject a custom env variable to force the creation of a new version + // even if the power is the same, which will force a cold start + lambdaFunctionsToSet.push({lambdaARN: lambdaARN, powerValue: powerValue, envVars: currentEnvVars, alias: alias}); + } + } } + lambdaFunctionsToSet.push({lambdaARN: lambdaARN, powerValue: power, envVars: envVars}); - await utils.setLambdaPower(lambdaARN, initialPower); + const returnObj = { + initConfigurations: lambdaFunctionsToSet, + iterator: { + index: 0, + count: lambdaFunctionsToSet.length, + continue: true, + } + } + return returnObj; +}; - return powerValues; +module.exports.handlerTest = async(event, context) => { + const iterator = event.powerValues.iterator; + const initConfigurations = event.powerValues.initConfigurations; + const aliases = event.powerValues.aliases || []; + const currIdx = iterator.index; + const currConfig = initConfigurations[currIdx]; + + // publish version + await utils.createPowerConfiguration(currConfig.lambdaARN, currConfig.powerValue, currConfig.alias, currConfig.envVars); + if(typeof currConfig.alias !== 'undefined'){ + aliases.push(currConfig.alias); + } + + // update iterator + iterator.index++; + iterator.continue = (iterator.index < iterator.count); + if(!iterator.continue){ + delete event.powerValues.initConfigurations; + } + event.powerValues.aliases = aliases; + return event.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/utils.js b/lambda/utils.js index 875ec08..d2b7c8f 100644 --- a/lambda/utils.js +++ b/lambda/utils.js @@ -25,6 +25,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 = []; @@ -69,14 +77,18 @@ 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, envVars) => { try { - await utils.setLambdaPower(lambdaARN, value); + await utils.setLambdaPower(lambdaARN, value, envVars); // wait for functoin update to complete await utils.waitForFunctionUpdate(lambdaARN); const {Version} = await utils.publishLambdaVersion(lambdaARN); + 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); @@ -140,7 +152,11 @@ module.exports.getLambdaPower = async(lambdaARN) => { }; const lambda = utils.lambdaClientFromARN(lambdaARN); const config = await lambda.getFunctionConfiguration(params).promise(); - return config.MemorySize; + return { + power: config.MemorySize, + // we need to fetch env vars only to add a new one and force a cold start + envVars: (config.Environment || {}).Variables || {}, + }; }; /** @@ -175,11 +191,12 @@ 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, envVars) => { console.log('Setting power to ', value); const params = { FunctionName: lambdaARN, MemorySize: parseInt(value, 10), + Environment: {Variables: envVars}, }; const lambda = utils.lambdaClientFromARN(lambdaARN); return lambda.updateFunctionConfiguration(params).promise(); @@ -543,6 +560,7 @@ module.exports.regionFromARN = (arn) => { return arn.split(':')[3]; }; +let client; module.exports.lambdaClientFromARN = (lambdaARN) => { const region = this.regionFromARN(lambdaARN); return new AWS.Lambda({region}); diff --git a/template.yml b/template.yml index e074841..a828d3e 100644 --- a/template.yml +++ b/template.yml @@ -131,6 +131,26 @@ Resources: - lambda:CreateAlias - lambda:UpdateAlias Resource: !Ref lambdaResource + initializer2: + Type: AWS::Serverless::Function + Properties: + CodeUri: lambda + Handler: initializer.handlerTest + Layers: + - !Ref SDKlayer + Policies: + - AWSLambdaBasicExecutionRole # Only logs + - Version: '2012-10-17' # allow Lambda actions + Statement: + - Effect: Allow + Action: + - lambda:GetAlias + - lambda:GetFunctionConfiguration + - lambda:PublishVersion + - lambda:UpdateFunctionConfiguration + - lambda:CreateAlias + - lambda:UpdateAlias + Resource: !Ref lambdaResource executorLogGroup: Type: AWS::Logs::LogGroup @@ -273,7 +293,14 @@ Resources: "Initializer": { "Type": "Task", "Resource": "${initializerArn}", - "Next": "Branching", + "Next": "Initializer2", + "ResultPath": "$.powerValues", + "TimeoutSeconds": ${totalExecutionTimeout} + }, + "Initializer2": { + "Type": "Task", + "Resource": "${initializer2Arn}", + "Next": "IsCountReached", "ResultPath": "$.powerValues", "TimeoutSeconds": ${totalExecutionTimeout}, "Catch": [{ @@ -282,10 +309,21 @@ Resources: "ResultPath": "$.error" }] }, + "IsCountReached": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.powerValues.iterator.continue", + "BooleanEquals": true, + "Next": "Initializer2" + } + ], + "Default": "Branching" + }, "Branching": { "Type": "Map", "Next": "Cleaner", - "ItemsPath": "$.powerValues", + "ItemsPath": "$.powerValues.aliases", "ResultPath": "$.stats", "ItemSelector": { "input.$": "$", @@ -347,6 +385,7 @@ Resources: } }' - initializerArn: !GetAtt initializer.Arn + initializer2Arn: !GetAtt initializer2.Arn executorArn: !GetAtt executor.Arn cleanerArn: !GetAtt cleaner.Arn analyzerArn: !GetAtt analyzer.Arn From 89a255be2d4f5aa39bbce6725c8040c006d00a8c Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Thu, 11 May 2023 16:57:38 +0200 Subject: [PATCH 02/38] Working implementation --- lambda/cleaner.js | 25 +++++++++++++++++-------- lambda/executor.js | 36 +++++++++++++++++++++++++----------- lambda/initializer.js | 11 +++++++---- lambda/utils.js | 19 +++++++++++++++---- template.yml | 16 ++++++++-------- 5 files changed, 72 insertions(+), 35 deletions(-) diff --git a/lambda/cleaner.js b/lambda/cleaner.js index 4c1d3c3..a61906c 100644 --- a/lambda/cleaner.js +++ b/lambda/cleaner.js @@ -7,13 +7,15 @@ const utils = require('./utils'); */ module.exports.handler = async(event, context) => { - const {lambdaARN, powerValues} = event; + const { + lambdaARN, + aliases, + } = extractDataFromInput(event); - validateInput(lambdaARN, powerValues); // may throw + validateInput(lambdaARN, aliases); // may throw - const ops = powerValues.map(async(value) => { - const alias = 'RAM' + value; - await cleanup(lambdaARN, alias); // may throw + const ops = aliases.map(async(alias) => { + await cleanup(lambdaARN, alias); }); // run everything in parallel and wait until completed @@ -22,12 +24,19 @@ module.exports.handler = async(event, context) => { return 'OK'; }; -const validateInput = (lambdaARN, powerValues) => { +const extractDataFromInput = (event) => { + return { + lambdaARN: event.lambdaARN, + aliases: event.powerValues.aliases, + }; +}; + +const validateInput = (lambdaARN, aliases) => { if (!lambdaARN) { throw new Error('Missing or empty lambdaARN'); } - if (!powerValues || !powerValues.length) { - throw new Error('Missing or empty power values'); + if (!aliases || !aliases.length) { + throw new Error('Missing or empty alias values'); } }; diff --git a/lambda/executor.js b/lambda/executor.js index a77ebaa..c96d025 100644 --- a/lambda/executor.js +++ b/lambda/executor.js @@ -19,7 +19,6 @@ module.exports.handler = async(event, context) => { let { lambdaARN, value, - alias, num, enableParallel, payload, @@ -39,11 +38,12 @@ module.exports.handler = async(event, context) => { num = 1; } - const lambdaAlias = alias; + const lambdaAlias = 'RAM' + value; let results; - // fetch architecture from $LATEST - const {architecture, isPending} = await utils.getLambdaConfig(lambdaARN, lambdaAlias); + let aliasToInvoke = utils.buildAliasString(lambdaAlias, onlyColdStarts, 0); + const {architecture, isPending} = await utils.getLambdaConfig(lambdaARN, aliasToInvoke); + console.log(`Detected architecture type: ${architecture}, isPending: ${isPending}`); // pre-generate an array of N payloads @@ -56,11 +56,12 @@ module.exports.handler = async(event, context) => { payloads: payloads, preARN: preProcessorARN, postARN: postProcessorARN, + onlyColdStarts: onlyColdStarts, sleepBetweenRunsMs: sleepBetweenRunsMs, }; // wait if the function/alias state is Pending - if (isPending) { + if (isPending && !onlyColdStarts) { await utils.waitForAliasActive(lambdaARN, lambdaAlias); console.log('Alias active'); } @@ -129,8 +130,7 @@ const extractDataFromInput = async(event) => { const discardTopBottom = extractDiscardTopBottomValue(input); const sleepBetweenRunsMs = extractSleepTime(input); return { - alias: event.value, - value: event.value.match(/\d+/g)[0], + value: parseInt(event.value, 10), lambdaARN: input.lambdaARN, num: parseInt(input.num, 10), enableParallel: !!input.parallelInvocation, @@ -139,15 +139,25 @@ const extractDataFromInput = async(event) => { preProcessorARN: input.preProcessorARN, postProcessorARN: input.postProcessorARN, discardTopBottom: discardTopBottom, + onlyColdStarts: !!input.onlyColdStarts, sleepBetweenRunsMs: sleepBetweenRunsMs, }; }; -const runInParallel = async({num, lambdaARN, lambdaAlias, payloads, preARN, postARN}) => { +const runInParallel = async({num, lambdaARN, lambdaAlias, payloads, preARN, postARN, 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); + let aliasToInvoke = utils.buildAliasString(lambdaAlias, onlyColdStarts, i); + if (onlyColdStarts){ + try { + await utils.waitForAliasActive(lambdaARN, aliasToInvoke); + } catch (e){ + console.log(e); + } + console.log(`${aliasToInvoke} is active`); + } + const {invocationResults, actualPayload} = await utils.invokeLambdaWithProcessors(lambdaARN, aliasToInvoke, payloads[i], preARN, postARN); // invocation errors return 200 and contain FunctionError and Payload if (invocationResults.FunctionError) { throw new Error(`Invocation error (running in parallel): ${invocationResults.Payload} with payload ${JSON.stringify(actualPayload)}`); @@ -159,11 +169,15 @@ const runInParallel = async({num, lambdaARN, lambdaAlias, payloads, preARN, post return results; }; -const runInSeries = async({num, lambdaARN, lambdaAlias, payloads, preARN, postARN, sleepBetweenRunsMs}) => { +const runInSeries = async({num, lambdaARN, lambdaAlias, payloads, preARN, postARN, onlyColdStarts, sleepBetweenRunsMs }) => { 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); + if (onlyColdStarts){ + await utils.waitForAliasActive(lambdaARN, aliasToInvoke); + } + const {invocationResults, actualPayload} = await utils.invokeLambdaWithProcessors(lambdaARN, aliasToInvoke, payloads[i], preARN, postARN); // invocation errors return 200 and contain FunctionError and Payload if (invocationResults.FunctionError) { throw new Error(`Invocation error (running in series): ${invocationResults.Payload} with payload ${JSON.stringify(actualPayload)}`); diff --git a/lambda/initializer.js b/lambda/initializer.js index 7aeaf0f..368d0ca 100644 --- a/lambda/initializer.js +++ b/lambda/initializer.js @@ -32,9 +32,10 @@ module.exports.handler = async(event, context) => { for (let n of utils.range(num)){ let alias = utils.buildAliasString(baseAlias, onlyColdStarts, n); const currentEnvVars = { - LambdaPowerTuningForceColdStart: alias, ...envVars, }; + // set Env Var to a unique value to force version publishing + currentEnvVars.LambdaPowerTuningForceColdStart = alias; // here we inject a custom env variable to force the creation of a new version // even if the power is the same, which will force a cold start lambdaFunctionsToSet.push({lambdaARN: lambdaARN, powerValue: powerValue, envVars: currentEnvVars, alias: alias}); @@ -49,21 +50,23 @@ module.exports.handler = async(event, context) => { index: 0, count: lambdaFunctionsToSet.length, continue: true, - } + }, + powerValues: powerValues } return returnObj; }; -module.exports.handlerTest = async(event, context) => { +module.exports.versionPublisher = async(event, context) => { const iterator = event.powerValues.iterator; const initConfigurations = event.powerValues.initConfigurations; const aliases = event.powerValues.aliases || []; const currIdx = iterator.index; const currConfig = initConfigurations[currIdx]; - // publish version + // publish version & assign alias await utils.createPowerConfiguration(currConfig.lambdaARN, currConfig.powerValue, currConfig.alias, currConfig.envVars); if(typeof currConfig.alias !== 'undefined'){ + // keep track of all aliases aliases.push(currConfig.alias); } diff --git a/lambda/utils.js b/lambda/utils.js index d2b7c8f..49b1ddb 100644 --- a/lambda/utils.js +++ b/lambda/utils.js @@ -138,7 +138,7 @@ module.exports.waitForAliasActive = async(lambdaARN, alias) => { }, }; const lambda = utils.lambdaClientFromARN(lambdaARN); - return lambda.waitFor('functionActive', params).promise(); + return lambda.waitFor('functionActiveV2', params).promise(); }; /** @@ -296,8 +296,14 @@ module.exports.invokeLambdaWithProcessors = async(lambdaARN, alias, payload, pre } } - // invoke function to be power-tuned - const invocationResults = await utils.invokeLambda(lambdaARN, alias, actualPayload); + let invocationResults; + try { + // invoke function to be power-tuned + invocationResults = await utils.invokeLambda(lambdaARN, alias, actualPayload); + } catch (e){ + console.log(`Invocation failed, ${alias}`); + console.err(e); + } // then invoke post-processor, if provided if (postARN) { @@ -563,7 +569,12 @@ module.exports.regionFromARN = (arn) => { let client; module.exports.lambdaClientFromARN = (lambdaARN) => { const region = this.regionFromARN(lambdaARN); - return new AWS.Lambda({region}); + // create a client only once + if (typeof client === 'undefined'){ + // set Max Retries to 10, increase the retry delay to 300 + client = new AWS.Lambda({region: region, maxRetries: 10, retryDelayOptions: {base: 300}}); + } + return client; }; /** diff --git a/template.yml b/template.yml index a828d3e..4135fce 100644 --- a/template.yml +++ b/template.yml @@ -131,11 +131,11 @@ Resources: - lambda:CreateAlias - lambda:UpdateAlias Resource: !Ref lambdaResource - initializer2: + versionPublisher: Type: AWS::Serverless::Function Properties: CodeUri: lambda - Handler: initializer.handlerTest + Handler: initializer.versionPublisher Layers: - !Ref SDKlayer Policies: @@ -293,13 +293,13 @@ Resources: "Initializer": { "Type": "Task", "Resource": "${initializerArn}", - "Next": "Initializer2", + "Next": "versionPublisher", "ResultPath": "$.powerValues", "TimeoutSeconds": ${totalExecutionTimeout} }, - "Initializer2": { + "versionPublisher": { "Type": "Task", - "Resource": "${initializer2Arn}", + "Resource": "${versionPublisherArn}", "Next": "IsCountReached", "ResultPath": "$.powerValues", "TimeoutSeconds": ${totalExecutionTimeout}, @@ -315,7 +315,7 @@ Resources: { "Variable": "$.powerValues.iterator.continue", "BooleanEquals": true, - "Next": "Initializer2" + "Next": "versionPublisher" } ], "Default": "Branching" @@ -323,7 +323,7 @@ Resources: "Branching": { "Type": "Map", "Next": "Cleaner", - "ItemsPath": "$.powerValues.aliases", + "ItemsPath": "$.powerValues.powerValues", "ResultPath": "$.stats", "ItemSelector": { "input.$": "$", @@ -385,7 +385,7 @@ Resources: } }' - initializerArn: !GetAtt initializer.Arn - initializer2Arn: !GetAtt initializer2.Arn + versionPublisherArn: !GetAtt versionPublisher.Arn executorArn: !GetAtt executor.Arn cleanerArn: !GetAtt cleaner.Arn analyzerArn: !GetAtt analyzer.Arn From 711e37d3dc0ab59464fc23f16bada52211651055 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Fri, 19 May 2023 12:11:00 +0100 Subject: [PATCH 03/38] Addressing PR feedback --- lambda/initializer.js | 41 ++++++----------------------------------- lambda/publisher.js | 36 ++++++++++++++++++++++++++++++++++++ lambda/utils.js | 2 +- template.yml | 6 +----- 4 files changed, 44 insertions(+), 41 deletions(-) create mode 100644 lambda/publisher.js diff --git a/lambda/initializer.js b/lambda/initializer.js index 368d0ca..0c78b73 100644 --- a/lambda/initializer.js +++ b/lambda/initializer.js @@ -18,7 +18,7 @@ module.exports.handler = async(event, context) => { validateInput(lambdaARN, num); // may throw // fetch initial $LATEST value so we can reset it later - const {power, envVars} = await utils.getLambdaPower(lambdaARN); + const {power} = await utils.getLambdaPower(lambdaARN); let lambdaFunctionsToSet = []; @@ -27,22 +27,17 @@ module.exports.handler = async(event, context) => { for (let powerValue of powerValues){ const baseAlias = 'RAM' + powerValue; if (!onlyColdStarts){ - lambdaFunctionsToSet.push({lambdaARN: lambdaARN, powerValue: powerValue, envVars: envVars, alias: baseAlias}); + lambdaFunctionsToSet.push({powerValue: powerValue, alias: baseAlias}); } else { for (let n of utils.range(num)){ let alias = utils.buildAliasString(baseAlias, onlyColdStarts, n); - const currentEnvVars = { - ...envVars, - }; - // set Env Var to a unique value to force version publishing - currentEnvVars.LambdaPowerTuningForceColdStart = alias; // here we inject a custom env variable to force the creation of a new version // even if the power is the same, which will force a cold start - lambdaFunctionsToSet.push({lambdaARN: lambdaARN, powerValue: powerValue, envVars: currentEnvVars, alias: alias}); + lambdaFunctionsToSet.push({powerValue: powerValue, alias: alias}); } } } - lambdaFunctionsToSet.push({lambdaARN: lambdaARN, powerValue: power, envVars: envVars}); + lambdaFunctionsToSet.push({powerValue: power}); const returnObj = { initConfigurations: lambdaFunctionsToSet, @@ -51,35 +46,11 @@ module.exports.handler = async(event, context) => { count: lambdaFunctionsToSet.length, continue: true, }, - powerValues: powerValues - } + powerValues: powerValues, + }; return returnObj; }; -module.exports.versionPublisher = async(event, context) => { - const iterator = event.powerValues.iterator; - const initConfigurations = event.powerValues.initConfigurations; - const aliases = event.powerValues.aliases || []; - const currIdx = iterator.index; - const currConfig = initConfigurations[currIdx]; - - // publish version & assign alias - await utils.createPowerConfiguration(currConfig.lambdaARN, currConfig.powerValue, currConfig.alias, currConfig.envVars); - if(typeof currConfig.alias !== 'undefined'){ - // keep track of all aliases - aliases.push(currConfig.alias); - } - - // update iterator - iterator.index++; - iterator.continue = (iterator.index < iterator.count); - if(!iterator.continue){ - delete event.powerValues.initConfigurations; - } - event.powerValues.aliases = aliases; - return event.powerValues; -} - const extractDataFromInput = (event) => { return { diff --git a/lambda/publisher.js b/lambda/publisher.js new file mode 100644 index 0000000..3f195ba --- /dev/null +++ b/lambda/publisher.js @@ -0,0 +1,36 @@ +'use strict'; + +const utils = require('./utils'); + +module.exports.handler = async(event, context) => { + const iterator = event.powerValues.iterator; + const initConfigurations = event.powerValues.initConfigurations; + const aliases = event.powerValues.aliases || []; + const currIdx = iterator.index; + const currConfig = initConfigurations[currIdx]; + currConfig.lambdaARN = event.lambdaARN; + + + const {envVars} = await utils.getLambdaPower(currConfig.lambdaARN); + if (typeof currConfig.alias !== 'undefined'){ + envVars.LambdaPowerTuningForceColdStart = currConfig.alias; + } else { + delete envVars.LambdaPowerTuningForceColdStart; + } + + // publish version & assign alias + await utils.createPowerConfiguration(currConfig.lambdaARN, currConfig.powerValue, currConfig.alias, envVars); + if (typeof currConfig.alias !== 'undefined') { + // keep track of all aliases + aliases.push(currConfig.alias); + } + + // update iterator + iterator.index++; + iterator.continue = (iterator.index < iterator.count); + if (!iterator.continue) { + delete event.powerValues.initConfigurations; + } + event.powerValues.aliases = aliases; + return event.powerValues; +}; diff --git a/lambda/utils.js b/lambda/utils.js index 49b1ddb..905b99e 100644 --- a/lambda/utils.js +++ b/lambda/utils.js @@ -302,7 +302,7 @@ module.exports.invokeLambdaWithProcessors = async(lambdaARN, alias, payload, pre invocationResults = await utils.invokeLambda(lambdaARN, alias, actualPayload); } catch (e){ console.log(`Invocation failed, ${alias}`); - console.err(e); + throw new Error(`Unknown error when trying to invoke Alias: ${alias}`); } // then invoke post-processor, if provided diff --git a/template.yml b/template.yml index 4135fce..4b99fd1 100644 --- a/template.yml +++ b/template.yml @@ -126,16 +126,12 @@ Resources: Action: - lambda:GetAlias - lambda:GetFunctionConfiguration - - lambda:PublishVersion - - lambda:UpdateFunctionConfiguration - - lambda:CreateAlias - - lambda:UpdateAlias Resource: !Ref lambdaResource versionPublisher: Type: AWS::Serverless::Function Properties: CodeUri: lambda - Handler: initializer.versionPublisher + Handler: publisher.handler Layers: - !Ref SDKlayer Policies: From 4720586357efaf2c34512edbf8697a9715eb0896 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 24 May 2023 12:30:27 +0200 Subject: [PATCH 04/38] add permission required for `functionActiveV2` --- template.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/template.yml b/template.yml index 4b99fd1..49338d1 100644 --- a/template.yml +++ b/template.yml @@ -170,6 +170,7 @@ Resources: Action: - lambda:InvokeFunction - lambda:GetFunctionConfiguration + - lambda:GetFunction Resource: !Ref lambdaResource - !If - S3BucketProvided # if S3 bucket is provided From f70677d97d0a6b2da104871cf31b718d2e7153e2 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 24 May 2023 12:31:12 +0200 Subject: [PATCH 05/38] add comments and tidy up --- lambda/initializer.js | 1 + lambda/publisher.js | 3 ++- lambda/utils.js | 16 +++++----------- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/lambda/initializer.js b/lambda/initializer.js index 0c78b73..70ae605 100644 --- a/lambda/initializer.js +++ b/lambda/initializer.js @@ -37,6 +37,7 @@ module.exports.handler = async(event, context) => { } } } + // Publish another version to revert the Lambda Function to its original configuration lambdaFunctionsToSet.push({powerValue: power}); const returnObj = { diff --git a/lambda/publisher.js b/lambda/publisher.js index 3f195ba..6d04d3f 100644 --- a/lambda/publisher.js +++ b/lambda/publisher.js @@ -12,13 +12,14 @@ module.exports.handler = async(event, context) => { const {envVars} = await utils.getLambdaPower(currConfig.lambdaARN); + // Alias may not exist when we are reverting the Lambda function to its original configuration if (typeof currConfig.alias !== 'undefined'){ envVars.LambdaPowerTuningForceColdStart = currConfig.alias; } else { delete envVars.LambdaPowerTuningForceColdStart; } - // publish version & assign alias + // publish version & assign alias (if present) await utils.createPowerConfiguration(currConfig.lambdaARN, currConfig.powerValue, currConfig.alias, envVars); if (typeof currConfig.alias !== 'undefined') { // keep track of all aliases diff --git a/lambda/utils.js b/lambda/utils.js index 905b99e..b885f50 100644 --- a/lambda/utils.js +++ b/lambda/utils.js @@ -81,7 +81,7 @@ module.exports.createPowerConfiguration = async(lambdaARN, value, alias, envVars try { await utils.setLambdaPower(lambdaARN, value, envVars); - // wait for functoin update to complete + // wait for function update to complete await utils.waitForFunctionUpdate(lambdaARN); const {Version} = await utils.publishLambdaVersion(lambdaARN); @@ -296,14 +296,8 @@ module.exports.invokeLambdaWithProcessors = async(lambdaARN, alias, payload, pre } } - let invocationResults; - try { - // invoke function to be power-tuned - invocationResults = await utils.invokeLambda(lambdaARN, alias, actualPayload); - } catch (e){ - console.log(`Invocation failed, ${alias}`); - throw new Error(`Unknown error when trying to invoke Alias: ${alias}`); - } + // invoke function to be power-tuned + const invocationResults = await utils.invokeLambda(lambdaARN, alias, actualPayload); // then invoke post-processor, if provided if (postARN) { @@ -571,8 +565,8 @@ module.exports.lambdaClientFromARN = (lambdaARN) => { const region = this.regionFromARN(lambdaARN); // create a client only once if (typeof client === 'undefined'){ - // set Max Retries to 10, increase the retry delay to 300 - client = new AWS.Lambda({region: region, maxRetries: 10, retryDelayOptions: {base: 300}}); + // set Max Retries to 20, increase the retry delay to 500 + client = new AWS.Lambda({region: region, maxRetries: 20, retryDelayOptions: {base: 500}}); } return client; }; From 0445f329510bebde0a945352766d141f963dcc89 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 24 May 2023 16:46:14 +0200 Subject: [PATCH 06/38] tidy up --- lambda/executor.js | 6 +----- lambda/publisher.js | 35 +++++++++++++++++++++++++---------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/lambda/executor.js b/lambda/executor.js index c96d025..0e2dd4e 100644 --- a/lambda/executor.js +++ b/lambda/executor.js @@ -150,11 +150,7 @@ const runInParallel = async({num, lambdaARN, lambdaAlias, payloads, preARN, post const invocations = utils.range(num).map(async(_, i) => { let aliasToInvoke = utils.buildAliasString(lambdaAlias, onlyColdStarts, i); if (onlyColdStarts){ - try { - await utils.waitForAliasActive(lambdaARN, aliasToInvoke); - } catch (e){ - console.log(e); - } + await utils.waitForAliasActive(lambdaARN, aliasToInvoke); console.log(`${aliasToInvoke} is active`); } const {invocationResults, actualPayload} = await utils.invokeLambdaWithProcessors(lambdaARN, aliasToInvoke, payloads[i], preARN, postARN); diff --git a/lambda/publisher.js b/lambda/publisher.js index 6d04d3f..26df22b 100644 --- a/lambda/publisher.js +++ b/lambda/publisher.js @@ -2,16 +2,10 @@ const utils = require('./utils'); -module.exports.handler = async(event, context) => { - const iterator = event.powerValues.iterator; - const initConfigurations = event.powerValues.initConfigurations; - const aliases = event.powerValues.aliases || []; - const currIdx = iterator.index; - const currConfig = initConfigurations[currIdx]; - currConfig.lambdaARN = event.lambdaARN; - - const {envVars} = await utils.getLambdaPower(currConfig.lambdaARN); +module.exports.handler = async(event, context) => { + const {iterator, aliases, currConfig, lambdaARN} = validateInputs(event); + const {envVars} = await utils.getLambdaPower(lambdaARN); // Alias may not exist when we are reverting the Lambda function to its original configuration if (typeof currConfig.alias !== 'undefined'){ envVars.LambdaPowerTuningForceColdStart = currConfig.alias; @@ -20,7 +14,7 @@ module.exports.handler = async(event, context) => { } // publish version & assign alias (if present) - await utils.createPowerConfiguration(currConfig.lambdaARN, currConfig.powerValue, currConfig.alias, envVars); + await utils.createPowerConfiguration(lambdaARN, currConfig.powerValue, currConfig.alias, envVars); if (typeof currConfig.alias !== 'undefined') { // keep track of all aliases aliases.push(currConfig.alias); @@ -35,3 +29,24 @@ module.exports.handler = async(event, context) => { event.powerValues.aliases = aliases; return event.powerValues; }; +function validateInputs(event) { + if (!event.lambdaARN) { + throw new Error('Missing or empty lambdaARN'); + } + const lambdaARN = event.lambdaARN; + if (!(event.powerValues && event.powerValues.iterator && event.powerValues.initConfigurations)){ + throw new Error('Invalid input'); + } + const iterator = event.powerValues.iterator; + if (!(iterator.index >= 0 && iterator.index < iterator.count)){ + throw new Error('Invalid iterator input'); + } + const initConfigurations = event.powerValues.initConfigurations; + const aliases = event.powerValues.aliases || []; + const currIdx = iterator.index; + const currConfig = initConfigurations[currIdx]; + if (!(currConfig && currConfig.powerValue)){ + throw new Error('Invalid configuration'); + } + return {iterator, aliases, currConfig, lambdaARN}; +} From 4d8f554dc4b491d45a790c205d3818c27d615b89 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 24 May 2023 16:46:37 +0200 Subject: [PATCH 07/38] Fix tests and add more tests --- test/unit/test-lambda.js | 403 ++++++++++++++++++++++++++++++--------- test/unit/test-utils.js | 29 ++- 2 files changed, 334 insertions(+), 98 deletions(-) diff --git a/test/unit/test-lambda.js b/test/unit/test-lambda.js index e60de60..420d422 100644 --- a/test/unit/test-lambda.js +++ b/test/unit/test-lambda.js @@ -106,7 +106,10 @@ describe('Lambda Functions', async() => { sandBox.stub(utils, 'getLambdaPower') .callsFake(async() => { getLambdaPowerCounter++; - return 1024; + return { + power: 1024, + envVars: {}, + }; }); setLambdaPowerStub = sandBox.stub(utils, 'setLambdaPower') .callsFake(async() => { @@ -198,21 +201,132 @@ 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 aliases and versions', 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 N aliases and versions', async() => { + const generatedValues = await invokeForSuccess(handler, { lambdaARN: 'arnOK', num: 5 }); - expect(getLambdaPowerCounter).to.be(1); - expect(publishLambdaVersionCounter).to.be(powerValues.length); - expect(createLambdaAliasCounter).to.be(powerValues.length); - expect(waitForFunctionUpdateCounter).to.be(powerValues.length); + // +1 because it will also reset power to its initial value + 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', powerValues: {}}, + {lambdaARN: 'arnOK', + powerValues: { + initConfigurations: [{ + powerValue: 512, + alias: 'RAM512', + }], + }, + }, + {lambdaARN: 'arnOK', + powerValues: { + iterator: { + index: 1, + count: 1, + }, + }, + }, + {lambdaARN: 'arnOK', + powerValues: { + initConfigurations: [{ + powerValue: 512, + alias: 'RAM512', + }, { + powerValue: 1024, + alias: 'RAM1024', + }], + iterator: { + index: 2, + count: 3, + }, + }, + }, + {lambdaARN: 'arnOK', + powerValues: { + 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', async() => { + + const aliasValue = 'RAM512'; + const originalIndex = 0; + const generatedValues = await invokeForSuccess(handler, { + lambdaARN: 'arnOK', + powerValues: { + initConfigurations: [{ + powerValue: 512, + alias: aliasValue, + }], + iterator: { + index: originalIndex, + count: 1, + }, + }}); + expect(getLambdaPowerCounter).to.be(1); + 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(originalIndex + 1); // index should be incremented by 1 + expect(generatedValues.iterator.continue).to.be(false); // the iterator should be set to continue=false + expect(generatedValues.aliases.length).to.be(1); + expect(generatedValues.aliases[0]).to.be(aliasValue); + }); + it('should publish the version even if an alias is not specified', async() => { + await invokeForSuccess(handler, { + lambdaARN: 'arnOK', + powerValues: { + initConfigurations: [{ + powerValue: 512, + }], + iterator: { + index: 0, + count: 1, + }, + }}); + }); it('should update an alias if it already exists', async() => { getLambdaAliasStub && getLambdaAliasStub.restore(); getLambdaAliasStub = sandBox.stub(utils, 'getLambdaAlias') @@ -225,22 +339,21 @@ describe('Lambda Functions', async() => { throw error; } }); - await invokeForSuccess(handler, { lambdaARN: 'arnOK', num: 5 }); + await invokeForSuccess(handler, { + lambdaARN: 'arnOK', + powerValues: { + 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() => { @@ -249,7 +362,18 @@ describe('Lambda Functions', async() => { .callsFake(async() => { throw new Error('Something went wrong'); }); - await invokeForFailure(handler, { lambdaARN: 'arnOK', num: 5 }); + await invokeForFailure(handler, { + lambdaARN: 'arnOK', + powerValues: { + initConfigurations: [{ + powerValue: 128, + alias: 'RAM128', + }], + iterator: { + index: 0, + count: 1, + }, + }}); expect(waitForFunctionUpdateCounter).to.be(0); }); @@ -261,10 +385,20 @@ describe('Lambda Functions', async() => { error.code = 'VeryBadError'; throw error; }); - await invokeForFailure(handler, { lambdaARN: 'arnOK', num: 5 }); + await invokeForFailure(handler, { + lambdaARN: 'arnOK', + powerValues: { + initConfigurations: [{ + powerValue: 128, + alias: 'RAM128', + }], + iterator: { + index: 0, + count: 1, + }, + }}); expect(waitForFunctionUpdateCounter).to.be(1); }); - }); describe('cleaner', async() => { @@ -274,10 +408,11 @@ describe('Lambda Functions', async() => { let invalidEvents = [ null, {}, - { lambdaARN: null }, - { lambdaARN: '' }, - { lambdaARN: false }, - { lambdaARN: 0 }, + { lambdaARN: null, powerValues: { aliases: ['RAM128']}}, + { lambdaARN: '', powerValues: { aliases: ['RAM128']}}, + { lambdaARN: false, powerValues: { aliases: ['RAM128']}}, + { lambdaARN: 0, powerValues: { aliases: ['RAM128']}}, + { lambdaARN: '', powerValues: { aliases: ['RAM128']}}, ]; invalidEvents.forEach(async(event) => { @@ -286,6 +421,18 @@ describe('Lambda Functions', async() => { }); }); + invalidEvents = [ + { lambdaARN: 'arnOK'}, + { lambdaARN: 'arnOK', powerValues: {}}, + { lambdaARN: 'arnOK', powerValues: { aliases: []}}, + ]; + + invalidEvents.forEach(async(event) => { + it('should explode if invoked without valid aliases - ' + JSON.stringify(event), async() => { + await invokeForFailure(handler, event); + }); + }); + it('should explode if invoked without powerValues', async() => { await invokeForFailure(handler, {lambdaARN: 'arnOK'}); }); @@ -308,7 +455,7 @@ describe('Lambda Functions', async() => { }); }); - const eventOK = { lambdaARN: 'arnOK', powerValues: ['128', '256', '512'] }; + const eventOK = { lambdaARN: 'arnOK', powerValues: {aliases: ['RAM128', 'RAM256', 'RAM512'] }}; it('should invoke the given cb, when done', async() => { await invokeForSuccess(handler, eventOK); @@ -1251,72 +1398,72 @@ describe('Lambda Functions', async() => { 27.7, ]; + 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', + }, + ]; + 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') @@ -1342,6 +1489,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.7); + }); + + 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 81d2df1..2932fd8 100644 --- a/test/unit/test-utils.js +++ b/test/unit/test-utils.js @@ -24,7 +24,13 @@ const sandBox = sinon.createSandbox(); // AWS SDK mocks AWS.mock('Lambda', 'getAlias', {}); -AWS.mock('Lambda', 'getFunctionConfiguration', {MemorySize: 1024, State: 'Active', LastUpdateStatus: 'Successful', Architectures: ['x86_64']}); +AWS.mock('Lambda', 'getFunctionConfiguration', { + MemorySize: 1024, + State: 'Active', + LastUpdateStatus: 'Successful', + Architectures: ['x86_64'], + Environment: {Variables: {TEST: 'OK'}}, +}); AWS.mock('Lambda', 'updateFunctionConfiguration', {}); AWS.mock('Lambda', 'publishVersion', {}); AWS.mock('Lambda', 'deleteFunction', {}); @@ -106,9 +112,26 @@ describe('Lambda Utils', () => { }); describe('getLambdaPower', () => { - it('should return the memory value', async() => { + it('should return the power value and env vars', async() => { + const value = await utils.getLambdaPower('arn:aws:lambda:us-east-1:XXX:function:YYY'); + expect(value.power).to.be(1024); + expect(value.envVars).to.be.an('object'); + expect(value.envVars.TEST).to.be('OK'); + }); + + it('should return the power value and env vars even when empty env', async() => { + AWS.remock('Lambda', 'getFunctionConfiguration', { + MemorySize: 1024, + State: 'Active', + LastUpdateStatus: 'Successful', + Architectures: ['x86_64'], + Environment: null, // 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.envVars).to.be.an('object'); + expect(value.envVars.TEST).to.be(undefined); }); }); From f6d7ad1410e6dcc09099bf47d6bb6f3346cb83da Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 24 May 2023 17:38:12 +0200 Subject: [PATCH 08/38] Modify the Terraform template to match the changes in the SAM template --- terraform/module/json_files/executor.json | 3 +- terraform/module/json_files/initializer.json | 6 +-- terraform/module/json_files/publisher.json | 17 +++++++++ .../module/json_files/state_machine.json | 28 +++++++++++--- terraform/module/lambda.tf | 37 +++++++++++++++++++ terraform/module/locals.tf | 8 ++++ terraform/module/policies.tf | 13 +++++++ terraform/module/roles.tf | 7 ++++ 8 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 terraform/module/json_files/publisher.json diff --git a/terraform/module/json_files/executor.json b/terraform/module/json_files/executor.json index 344235b..7ff16ca 100644 --- a/terraform/module/json_files/executor.json +++ b/terraform/module/json_files/executor.json @@ -5,7 +5,8 @@ "Effect": "Allow", "Action": [ "lambda:InvokeFunction", - "lambda:GetFunctionConfiguration" + "lambda:GetFunctionConfiguration", + "lambda:GetFunction" ], "Resource": "arn:aws:lambda:*:${account_id}:function:*" } diff --git a/terraform/module/json_files/initializer.json b/terraform/module/json_files/initializer.json index 4f3b81c..f202fa9 100644 --- a/terraform/module/json_files/initializer.json +++ b/terraform/module/json_files/initializer.json @@ -5,11 +5,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 0000000..53ea18a --- /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 e63cc5d..cfbcdeb 100644 --- a/terraform/module/json_files/state_machine.json +++ b/terraform/module/json_files/state_machine.json @@ -5,19 +5,37 @@ "Initializer": { "Type": "Task", "Resource": "${initializerArn}", - "Next": "Branching", + "Next": "versionPublisher", + "ResultPath": "$.powerValues", + "TimeoutSeconds": 600 + }, + "versionPublisher": { + "Type": "Task", + "Resource": "${versionPublisherArn}", + "Next": "IsCountReached", "ResultPath": "$.powerValues", "TimeoutSeconds": 600, "Catch": [{ - "ErrorEquals": [ "States.ALL" ], - "Next": "CleanUpOnError", - "ResultPath": "$.error" + "ErrorEquals": [ "States.ALL" ], + "Next": "CleanUpOnError", + "ResultPath": "$.error" }] }, + "IsCountReached": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.powerValues.iterator.continue", + "BooleanEquals": true, + "Next": "versionPublisher" + } + ], + "Default": "Branching" + }, "Branching": { "Type": "Map", "Next": "Cleaner", - "ItemsPath": "$.powerValues", + "ItemsPath": "$.powerValues.powerValues", "ResultPath": "$.stats", "ItemSelector": { "input.$": "$", diff --git a/terraform/module/lambda.tf b/terraform/module/lambda.tf index f8f91bd..50fdc17 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 = "nodejs16.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 e7efa4f..0b148ed 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, + versionPublisherArn = 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 0f69fff..593c493 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 b6ac35b..f558683 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 From 32f9533197cd692725ec41eb3cc256b11aa32d1d Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 24 May 2023 17:52:32 +0200 Subject: [PATCH 09/38] preserve env vars when optimising --- lambda/optimizer.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lambda/optimizer.js b/lambda/optimizer.js index c301850..84af89c 100644 --- a/lambda/optimizer.js +++ b/lambda/optimizer.js @@ -26,7 +26,8 @@ module.exports.handler = async(event, context) => { await utils.setLambdaPower(lambdaARN, optimalValue); } else { // create/update alias - await utils.createPowerConfiguration(lambdaARN, optimalValue, autoOptimizeAlias); + const {envVars} = await utils.getLambdaPower(lambdaARN); + await utils.createPowerConfiguration(lambdaARN, optimalValue, autoOptimizeAlias, envVars); } return 'OK'; From c0dd5ffc65d4db2f5df12f77230a38af7c0a8d06 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 24 May 2023 17:56:32 +0200 Subject: [PATCH 10/38] remove redundant logic --- lambda/utils.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lambda/utils.js b/lambda/utils.js index b885f50..8760359 100644 --- a/lambda/utils.js +++ b/lambda/utils.js @@ -96,13 +96,8 @@ module.exports.createPowerConfiguration = async(lambdaARN, value, alias, envVars await utils.createLambdaAlias(lambdaARN, alias, Version); } } catch (error) { - if (error.message && error.message.includes('Alias already exists')) { - // shouldn't happen, but nothing we can do in that case - console.log('OK, even if: ', error); - } else { - console.log('error during config creation for value ' + value); - throw error; // a real error :) - } + console.log('error during config creation for value ' + value); + throw error; // a real error :) } }; From 5a2e515df24e541407412c77d6120c6a3e0dd85d Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 8 May 2024 14:24:59 +0200 Subject: [PATCH 11/38] reintroduce changes that were removed in the merge --- statemachine/statemachine.asl.json | 29 ++++++++++++++++++++++++++--- template.yml | 3 ++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/statemachine/statemachine.asl.json b/statemachine/statemachine.asl.json index 30fb230..0f6288f 100644 --- a/statemachine/statemachine.asl.json +++ b/statemachine/statemachine.asl.json @@ -5,7 +5,7 @@ "Initializer": { "Type": "Task", "Resource": "${initializerArn}", - "Next": "Branching", + "Next": "versionPublisher", "ResultPath": "$.powerValues", "TimeoutSeconds": ${totalExecutionTimeout}, "Catch": [ @@ -16,10 +16,33 @@ } ] }, + "versionPublisher": { + "Type": "Task", + "Resource": "${versionPublisherArn}", + "Next": "IsCountReached", + "ResultPath": "$.powerValues", + "TimeoutSeconds": ${totalExecutionTimeout}, + "Catch": [{ + "ErrorEquals": [ "States.ALL" ], + "Next": "CleanUpOnError", + "ResultPath": "$.error" + }] + }, + "IsCountReached": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.powerValues.iterator.continue", + "BooleanEquals": true, + "Next": "versionPublisher" + } + ], + "Default": "Branching" + }, "Branching": { "Type": "Map", "Next": "Cleaner", - "ItemsPath": "$.powerValues", + "ItemsPath": "$.powerValues.powerValues", "ResultPath": "$.stats", "ItemSelector": { "input.$": "$", @@ -83,4 +106,4 @@ "End": true } } -} \ No newline at end of file +} diff --git a/template.yml b/template.yml index 8ecbf5c..4458ba6 100644 --- a/template.yml +++ b/template.yml @@ -295,6 +295,7 @@ Resources: DefinitionUri: statemachine/statemachine.asl.json DefinitionSubstitutions: initializerArn: !GetAtt initializer.Arn + versionPublisherArn: !GetAtt versionPublisher.Arn executorArn: !GetAtt executor.Arn cleanerArn: !GetAtt cleaner.Arn analyzerArn: !GetAtt analyzer.Arn @@ -303,4 +304,4 @@ Resources: Outputs: StateMachineARN: - Value: !Ref powerTuningStateMachine \ No newline at end of file + Value: !Ref powerTuningStateMachine From 93802fdc1f7e7abbc77db98955a70c6ddde3d34a Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 8 May 2024 14:35:39 +0200 Subject: [PATCH 12/38] PR feedback --- lambda/initializer.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lambda/initializer.js b/lambda/initializer.js index 70ae605..dbc6dda 100644 --- a/lambda/initializer.js +++ b/lambda/initializer.js @@ -20,31 +20,31 @@ module.exports.handler = async(event, context) => { // fetch initial $LATEST value so we can reset it later const {power} = await utils.getLambdaPower(lambdaARN); - let lambdaFunctionsToSet = []; + let initConfigurations = []; // reminder: configuration updates must run sequentially // (otherwise you get a ResourceConflictException) for (let powerValue of powerValues){ const baseAlias = 'RAM' + powerValue; if (!onlyColdStarts){ - lambdaFunctionsToSet.push({powerValue: powerValue, alias: baseAlias}); + 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 env variable to force the creation of a new version // even if the power is the same, which will force a cold start - lambdaFunctionsToSet.push({powerValue: powerValue, alias: alias}); + initConfigurations.push({powerValue: powerValue, alias: alias}); } } } // Publish another version to revert the Lambda Function to its original configuration - lambdaFunctionsToSet.push({powerValue: power}); + initConfigurations.push({powerValue: power}); const returnObj = { - initConfigurations: lambdaFunctionsToSet, + initConfigurations: initConfigurations, iterator: { index: 0, - count: lambdaFunctionsToSet.length, + count: initConfigurations.length, continue: true, }, powerValues: powerValues, From aa858385a955a3d88e1020a280ee8b80c9bebfde Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 8 May 2024 14:42:19 +0200 Subject: [PATCH 13/38] PR feedback --- lambda/publisher.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lambda/publisher.js b/lambda/publisher.js index 26df22b..2079c78 100644 --- a/lambda/publisher.js +++ b/lambda/publisher.js @@ -35,18 +35,18 @@ function validateInputs(event) { } const lambdaARN = event.lambdaARN; if (!(event.powerValues && event.powerValues.iterator && event.powerValues.initConfigurations)){ - throw new Error('Invalid input'); + throw new Error('Invalid iterator for initialization'); } const iterator = event.powerValues.iterator; if (!(iterator.index >= 0 && iterator.index < iterator.count)){ - throw new Error('Invalid iterator input'); + throw new Error(`Invalid iterator index: ${iterator.index}`); } const initConfigurations = event.powerValues.initConfigurations; const aliases = event.powerValues.aliases || []; const currIdx = iterator.index; const currConfig = initConfigurations[currIdx]; if (!(currConfig && currConfig.powerValue)){ - throw new Error('Invalid configuration'); + throw new Error(`Invalid init configuration: ${currConfig}`); } return {iterator, aliases, currConfig, lambdaARN}; } From 1656b7ee10a60249d6cf80dd0a7e76a7fec56c32 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 8 May 2024 19:22:04 +0200 Subject: [PATCH 14/38] PR feedback --- lambda/publisher.js | 35 ++++++++++++++++++------------ statemachine/statemachine.asl.json | 8 +++---- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/lambda/publisher.js b/lambda/publisher.js index 2079c78..05fe970 100644 --- a/lambda/publisher.js +++ b/lambda/publisher.js @@ -4,7 +4,10 @@ const utils = require('./utils'); module.exports.handler = async(event, context) => { - const {iterator, aliases, currConfig, lambdaARN} = validateInputs(event); + const {lambdaConfigurations, currConfig, lambdaARN} = validateInputs(event); + const currentIterator = lambdaConfigurations.iterator; + const aliases = lambdaConfigurations.aliases; + const {envVars} = await utils.getLambdaPower(lambdaARN); // Alias may not exist when we are reverting the Lambda function to its original configuration if (typeof currConfig.alias !== 'undefined'){ @@ -21,32 +24,36 @@ module.exports.handler = async(event, context) => { } // update iterator - iterator.index++; - iterator.continue = (iterator.index < iterator.count); - if (!iterator.continue) { - delete event.powerValues.initConfigurations; - } - event.powerValues.aliases = aliases; - return event.powerValues; + const updatedIterator = { + index: (currentIterator.index + 1), + count: currentIterator.count, + continue: ((currentIterator.index + 1) < currentIterator.count), + }; + const updatedLambdaConfigurations = { + initConfigurations: ((updatedIterator.continue) ? lambdaConfigurations.initConfigurations : undefined), + iterator: updatedIterator, + aliases: aliases, + powerValues: lambdaConfigurations.powerValues, + }; + return updatedLambdaConfigurations; }; function validateInputs(event) { if (!event.lambdaARN) { throw new Error('Missing or empty lambdaARN'); } const lambdaARN = event.lambdaARN; - if (!(event.powerValues && event.powerValues.iterator && event.powerValues.initConfigurations)){ + if (!(event.lambdaConfigurations && event.lambdaConfigurations.iterator && event.lambdaConfigurations.initConfigurations)){ throw new Error('Invalid iterator for initialization'); } - const iterator = event.powerValues.iterator; + const iterator = event.lambdaConfigurations.iterator; if (!(iterator.index >= 0 && iterator.index < iterator.count)){ throw new Error(`Invalid iterator index: ${iterator.index}`); } - const initConfigurations = event.powerValues.initConfigurations; - const aliases = event.powerValues.aliases || []; + const lambdaConfigurations = event.lambdaConfigurations; const currIdx = iterator.index; - const currConfig = initConfigurations[currIdx]; + const currConfig = lambdaConfigurations.initConfigurations[currIdx]; if (!(currConfig && currConfig.powerValue)){ throw new Error(`Invalid init configuration: ${currConfig}`); } - return {iterator, aliases, currConfig, lambdaARN}; + return {lambdaConfigurations, currConfig, lambdaARN}; } diff --git a/statemachine/statemachine.asl.json b/statemachine/statemachine.asl.json index 0f6288f..2ea7c90 100644 --- a/statemachine/statemachine.asl.json +++ b/statemachine/statemachine.asl.json @@ -6,7 +6,7 @@ "Type": "Task", "Resource": "${initializerArn}", "Next": "versionPublisher", - "ResultPath": "$.powerValues", + "ResultPath": "$.lambdaConfigurations", "TimeoutSeconds": ${totalExecutionTimeout}, "Catch": [ { @@ -20,7 +20,7 @@ "Type": "Task", "Resource": "${versionPublisherArn}", "Next": "IsCountReached", - "ResultPath": "$.powerValues", + "ResultPath": "$.lambdaConfigurations", "TimeoutSeconds": ${totalExecutionTimeout}, "Catch": [{ "ErrorEquals": [ "States.ALL" ], @@ -32,7 +32,7 @@ "Type": "Choice", "Choices": [ { - "Variable": "$.powerValues.iterator.continue", + "Variable": "$.lambdaConfigurations.iterator.continue", "BooleanEquals": true, "Next": "versionPublisher" } @@ -42,7 +42,7 @@ "Branching": { "Type": "Map", "Next": "Cleaner", - "ItemsPath": "$.powerValues.powerValues", + "ItemsPath": "$.lambdaConfigurations.powerValues", "ResultPath": "$.stats", "ItemSelector": { "input.$": "$", From 79bb3b8cfab97e8dd2d399a98515efcccb6ec5de Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 8 May 2024 19:30:25 +0200 Subject: [PATCH 15/38] fix tests --- lambda/publisher.js | 2 +- test/unit/test-lambda.js | 38 +++++++++++++++++++------------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/lambda/publisher.js b/lambda/publisher.js index 05fe970..91cdb52 100644 --- a/lambda/publisher.js +++ b/lambda/publisher.js @@ -6,7 +6,7 @@ const utils = require('./utils'); module.exports.handler = async(event, context) => { const {lambdaConfigurations, currConfig, lambdaARN} = validateInputs(event); const currentIterator = lambdaConfigurations.iterator; - const aliases = lambdaConfigurations.aliases; + const aliases = lambdaConfigurations.aliases || []; const {envVars} = await utils.getLambdaPower(lambdaARN); // Alias may not exist when we are reverting the Lambda function to its original configuration diff --git a/test/unit/test-lambda.js b/test/unit/test-lambda.js index 6f41954..74adf4e 100644 --- a/test/unit/test-lambda.js +++ b/test/unit/test-lambda.js @@ -246,9 +246,9 @@ describe('Lambda Functions', async() => { const invalidEvents = [ { }, {lambdaARN: 'arnOK'}, - {lambdaARN: 'arnOK', powerValues: {}}, + {lambdaARN: 'arnOK', lambdaConfigurations: {}}, {lambdaARN: 'arnOK', - powerValues: { + lambdaConfigurations: { initConfigurations: [{ powerValue: 512, alias: 'RAM512', @@ -256,7 +256,7 @@ describe('Lambda Functions', async() => { }, }, {lambdaARN: 'arnOK', - powerValues: { + lambdaConfigurations: { iterator: { index: 1, count: 1, @@ -264,7 +264,7 @@ describe('Lambda Functions', async() => { }, }, {lambdaARN: 'arnOK', - powerValues: { + lambdaConfigurations: { initConfigurations: [{ powerValue: 512, alias: 'RAM512', @@ -279,7 +279,7 @@ describe('Lambda Functions', async() => { }, }, {lambdaARN: 'arnOK', - powerValues: { + lambdaConfigurations: { initConfigurations: [{ powerValue: 512, alias: 'RAM512', @@ -307,7 +307,7 @@ describe('Lambda Functions', async() => { const originalIndex = 0; const generatedValues = await invokeForSuccess(handler, { lambdaARN: 'arnOK', - powerValues: { + lambdaConfigurations: { initConfigurations: [{ powerValue: 512, alias: aliasValue, @@ -330,7 +330,7 @@ describe('Lambda Functions', async() => { it('should publish the version even if an alias is not specified', async() => { await invokeForSuccess(handler, { lambdaARN: 'arnOK', - powerValues: { + lambdaConfigurations: { initConfigurations: [{ powerValue: 512, }], @@ -353,7 +353,7 @@ describe('Lambda Functions', async() => { }); await invokeForSuccess(handler, { lambdaARN: 'arnOK', - powerValues: { + lambdaConfigurations: { initConfigurations: [{ powerValue: 128, alias: 'RAM128', @@ -376,7 +376,7 @@ describe('Lambda Functions', async() => { }); await invokeForFailure(handler, { lambdaARN: 'arnOK', - powerValues: { + lambdaConfigurations: { initConfigurations: [{ powerValue: 128, alias: 'RAM128', @@ -398,7 +398,7 @@ describe('Lambda Functions', async() => { }); await invokeForFailure(handler, { lambdaARN: 'arnOK', - powerValues: { + lambdaConfigurations: { initConfigurations: [{ powerValue: 128, alias: 'RAM128', @@ -419,11 +419,11 @@ describe('Lambda Functions', async() => { let invalidEvents = [ null, {}, - { lambdaARN: null, powerValues: { aliases: ['RAM128']}}, - { lambdaARN: '', powerValues: { aliases: ['RAM128']}}, - { lambdaARN: false, powerValues: { aliases: ['RAM128']}}, - { lambdaARN: 0, powerValues: { aliases: ['RAM128']}}, - { lambdaARN: '', powerValues: { aliases: ['RAM128']}}, + { lambdaARN: null, lambdaConfigurations: { aliases: ['RAM128']}}, + { lambdaARN: '', lambdaConfigurations: { aliases: ['RAM128']}}, + { lambdaARN: false, lambdaConfigurations: { aliases: ['RAM128']}}, + { lambdaARN: 0, lambdaConfigurations: { aliases: ['RAM128']}}, + { lambdaARN: '', lambdaConfigurations: { aliases: ['RAM128']}}, ]; invalidEvents.forEach(async(event) => { @@ -434,8 +434,8 @@ describe('Lambda Functions', async() => { invalidEvents = [ { lambdaARN: 'arnOK'}, - { lambdaARN: 'arnOK', powerValues: {}}, - { lambdaARN: 'arnOK', powerValues: { aliases: []}}, + { lambdaARN: 'arnOK', lambdaConfigurations: {}}, + { lambdaARN: 'arnOK', lambdaConfigurations: { aliases: []}}, ]; invalidEvents.forEach(async(event) => { @@ -444,7 +444,7 @@ describe('Lambda Functions', async() => { }); }); - it('should explode if invoked without powerValues', async() => { + it('should explode if invoked without lambdaConfigurations', async() => { await invokeForFailure(handler, {lambdaARN: 'arnOK'}); }); @@ -466,7 +466,7 @@ describe('Lambda Functions', async() => { }); }); - const eventOK = { lambdaARN: 'arnOK', powerValues: {aliases: ['RAM128', 'RAM256', 'RAM512'] }}; + const eventOK = { lambdaARN: 'arnOK', lambdaConfigurations: {aliases: ['RAM128', 'RAM256', 'RAM512'] }}; it('should invoke the given cb, when done', async() => { await invokeForSuccess(handler, eventOK); From 3c4b5d6897cc3d7c9dab35b3833e548c63cc081f Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Thu, 9 May 2024 13:47:40 +0200 Subject: [PATCH 16/38] fix cleaner.js --- lambda/cleaner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambda/cleaner.js b/lambda/cleaner.js index 4436a52..54bfbd8 100644 --- a/lambda/cleaner.js +++ b/lambda/cleaner.js @@ -28,7 +28,7 @@ module.exports.handler = async(event, context) => { const extractDataFromInput = (event) => { return { lambdaARN: event.lambdaARN, - aliases: event.powerValues.aliases, + aliases: event.lambdaConfigurations.aliases, }; }; From 69edf221333e152435da8564e482ae43786017f8 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 15 May 2024 19:12:48 +0200 Subject: [PATCH 17/38] remove singleton for client to avoid breaking existing use cases --- lambda/utils.js | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/lambda/utils.js b/lambda/utils.js index 0fff703..b439e3a 100644 --- a/lambda/utils.js +++ b/lambda/utils.js @@ -627,20 +627,13 @@ module.exports.regionFromARN = (arn) => { return arn.split(':')[3]; }; -let client; module.exports.lambdaClientFromARN = (lambdaARN) => { const region = this.regionFromARN(lambdaARN); - // create a client only once - if (typeof client === 'undefined'){ - // set Max Retries to 20, increase the retry delay to 500 - client = new LambdaClient({ - region, - maxAttempts: 20, - requestTimeout: 15 * 60 * 1000 - }) - - } - return client; + return new LambdaClient({ + region, + maxAttempts: 20, + requestTimeout: 15 * 60 * 1000, + }); }; /** From 6b9c85dd82509c48895908768f4e96493b9c85d5 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 15 May 2024 19:45:37 +0200 Subject: [PATCH 18/38] change logic to use Description instead of Env Vars --- lambda/initializer.js | 5 +++-- lambda/publisher.js | 10 +++++----- lambda/utils.js | 10 +++++----- statemachine/statemachine.asl.json | 8 ++++---- template.yml | 4 ++-- 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/lambda/initializer.js b/lambda/initializer.js index dbc6dda..d44eb1b 100644 --- a/lambda/initializer.js +++ b/lambda/initializer.js @@ -18,7 +18,8 @@ module.exports.handler = async(event, context) => { validateInput(lambdaARN, num); // may throw // fetch initial $LATEST value so we can reset it later - const {power} = await utils.getLambdaPower(lambdaARN); + const {power, description} = await utils.getLambdaPower(lambdaARN); + console.log(power, description); let initConfigurations = []; @@ -38,7 +39,7 @@ module.exports.handler = async(event, context) => { } } // Publish another version to revert the Lambda Function to its original configuration - initConfigurations.push({powerValue: power}); + initConfigurations.push({powerValue: power, description: description}); const returnObj = { initConfigurations: initConfigurations, diff --git a/lambda/publisher.js b/lambda/publisher.js index 91cdb52..d71ca7a 100644 --- a/lambda/publisher.js +++ b/lambda/publisher.js @@ -8,16 +8,16 @@ module.exports.handler = async(event, context) => { const currentIterator = lambdaConfigurations.iterator; const aliases = lambdaConfigurations.aliases || []; - const {envVars} = await utils.getLambdaPower(lambdaARN); + let description; // Alias may not exist when we are reverting the Lambda function to its original configuration if (typeof currConfig.alias !== 'undefined'){ - envVars.LambdaPowerTuningForceColdStart = currConfig.alias; + description = currConfig.alias; } else { - delete envVars.LambdaPowerTuningForceColdStart; + description = currConfig.description; } // publish version & assign alias (if present) - await utils.createPowerConfiguration(lambdaARN, currConfig.powerValue, currConfig.alias, envVars); + await utils.createPowerConfiguration(lambdaARN, currConfig.powerValue, currConfig.alias, description); if (typeof currConfig.alias !== 'undefined') { // keep track of all aliases aliases.push(currConfig.alias); @@ -53,7 +53,7 @@ function validateInputs(event) { const currIdx = iterator.index; const currConfig = lambdaConfigurations.initConfigurations[currIdx]; if (!(currConfig && currConfig.powerValue)){ - throw new Error(`Invalid init configuration: ${currConfig}`); + throw new Error(`Invalid init configuration: ${JSON.stringify(currConfig)}`); } return {lambdaConfigurations, currConfig, lambdaARN}; } diff --git a/lambda/utils.js b/lambda/utils.js index b439e3a..5688f86 100644 --- a/lambda/utils.js +++ b/lambda/utils.js @@ -84,9 +84,9 @@ module.exports.verifyAliasExistance = async(lambdaARN, alias) => { /** * Update power, publish new version, and create/update alias. */ -module.exports.createPowerConfiguration = async(lambdaARN, value, alias, envVars) => { +module.exports.createPowerConfiguration = async(lambdaARN, value, alias, description) => { try { - await utils.setLambdaPower(lambdaARN, value, envVars); + await utils.setLambdaPower(lambdaARN, value, description); // wait for function update to complete await utils.waitForFunctionUpdate(lambdaARN); @@ -157,7 +157,7 @@ module.exports.getLambdaPower = async(lambdaARN) => { return { power: config.MemorySize, // we need to fetch env vars only to add a new one and force a cold start - envVars: (config.Environment || {}).Variables || {}, + description: config.Description, }; }; @@ -193,12 +193,12 @@ module.exports.getLambdaConfig = async(lambdaARN, alias) => { /** * Update a given Lambda Function's memory size (always $LATEST version). */ -module.exports.setLambdaPower = (lambdaARN, value, envVars) => { +module.exports.setLambdaPower = (lambdaARN, value, description) => { console.log('Setting power to ', value); const params = { FunctionName: lambdaARN, MemorySize: parseInt(value, 10), - Environment: {Variables: envVars}, + Description: description, }; const lambda = utils.lambdaClientFromARN(lambdaARN); return lambda.send(new UpdateFunctionConfigurationCommand(params)); diff --git a/statemachine/statemachine.asl.json b/statemachine/statemachine.asl.json index 2ea7c90..79d9110 100644 --- a/statemachine/statemachine.asl.json +++ b/statemachine/statemachine.asl.json @@ -5,7 +5,7 @@ "Initializer": { "Type": "Task", "Resource": "${initializerArn}", - "Next": "versionPublisher", + "Next": "Publisher", "ResultPath": "$.lambdaConfigurations", "TimeoutSeconds": ${totalExecutionTimeout}, "Catch": [ @@ -16,9 +16,9 @@ } ] }, - "versionPublisher": { + "Publisher": { "Type": "Task", - "Resource": "${versionPublisherArn}", + "Resource": "${publisherArn}", "Next": "IsCountReached", "ResultPath": "$.lambdaConfigurations", "TimeoutSeconds": ${totalExecutionTimeout}, @@ -34,7 +34,7 @@ { "Variable": "$.lambdaConfigurations.iterator.continue", "BooleanEquals": true, - "Next": "versionPublisher" + "Next": "Publisher" } ], "Default": "Branching" diff --git a/template.yml b/template.yml index 4458ba6..e8794bd 100644 --- a/template.yml +++ b/template.yml @@ -134,7 +134,7 @@ Resources: - lambda:GetAlias - lambda:GetFunctionConfiguration Resource: !Ref lambdaResource - versionPublisher: + publisher: Type: AWS::Serverless::Function Properties: CodeUri: lambda @@ -295,7 +295,7 @@ Resources: DefinitionUri: statemachine/statemachine.asl.json DefinitionSubstitutions: initializerArn: !GetAtt initializer.Arn - versionPublisherArn: !GetAtt versionPublisher.Arn + publisherArn: !GetAtt publisher.Arn executorArn: !GetAtt executor.Arn cleanerArn: !GetAtt cleaner.Arn analyzerArn: !GetAtt analyzer.Arn From 331de582ed6ad944d43e2033a3e82c8dcb695119 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Thu, 16 May 2024 11:42:24 +0200 Subject: [PATCH 19/38] PR Feedback --- lambda/cleaner.js | 28 ++++++++++++++++++++++------ lambda/executor.js | 15 ++++++++++----- lambda/initializer.js | 7 +++---- lambda/publisher.js | 20 ++------------------ 4 files changed, 37 insertions(+), 33 deletions(-) diff --git a/lambda/cleaner.js b/lambda/cleaner.js index 54bfbd8..ed2a1a3 100644 --- a/lambda/cleaner.js +++ b/lambda/cleaner.js @@ -10,10 +10,15 @@ module.exports.handler = async(event, context) => { const { lambdaARN, - aliases, + powerValues, + onlyColdStarts, + num, } = extractDataFromInput(event); - validateInput(lambdaARN, aliases); // may throw + validateInput(lambdaARN, powerValues); // 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); @@ -25,19 +30,30 @@ module.exports.handler = async(event, context) => { return 'OK'; }; +const buildAliasListForCleanup = (lambdaARN, onlyColdStarts, powerValues, num) => { + let aliases; + if (onlyColdStarts){ + aliases = powerValues.map((powerValue) => utils.range(num).map((value) => utils.buildAliasString(`RAM${powerValue}`, onlyColdStarts, value))).flat(); + } else { + aliases = powerValues.map((powerValue) => utils.buildAliasString(`RAM${powerValue}`)); + } + return aliases; +}; const extractDataFromInput = (event) => { return { lambdaARN: event.lambdaARN, - aliases: event.lambdaConfigurations.aliases, + powerValues: event.lambdaConfigurations.powerValues, + onlyColdStarts: event.onlyColdStarts, + num: parseInt(event.num, 10), // use the default in case it was not defined }; }; -const validateInput = (lambdaARN, aliases) => { +const validateInput = (lambdaARN, powerValues) => { if (!lambdaARN) { throw new Error('Missing or empty lambdaARN'); } - if (!aliases || !aliases.length) { - throw new Error('Missing or empty alias values'); + if (!powerValues || !powerValues.length) { + throw new Error('Missing or empty powerValues values'); } }; diff --git a/lambda/executor.js b/lambda/executor.js index 03ebd24..3438cb3 100644 --- a/lambda/executor.js +++ b/lambda/executor.js @@ -36,7 +36,9 @@ module.exports.handler = async(event, context) => { const lambdaAlias = 'RAM' + value; let results; + // 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}`); @@ -57,6 +59,7 @@ module.exports.handler = async(event, context) => { }; // wait if the function/alias state is Pending + // 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'); @@ -100,11 +103,13 @@ const extractDiscardTopBottomValue = (event) => { // extract discardTopBottom used to trim values from average duration let discardTopBottom = event.discardTopBottom; if (typeof discardTopBottom === 'undefined') { - if (event.onlyColdStarts){ - discardTopBottom = 0; - } else { - discardTopBottom = 0.2; - } + // 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); diff --git a/lambda/initializer.js b/lambda/initializer.js index d44eb1b..73db502 100644 --- a/lambda/initializer.js +++ b/lambda/initializer.js @@ -28,20 +28,20 @@ module.exports.handler = async(event, context) => { for (let powerValue of powerValues){ const baseAlias = 'RAM' + powerValue; if (!onlyColdStarts){ - initConfigurations.push({powerValue: powerValue, alias: baseAlias}); + initConfigurations.push({powerValue: powerValue, alias: baseAlias, description: `${description} - ${baseAlias}`}); } else { for (let n of utils.range(num)){ let alias = utils.buildAliasString(baseAlias, onlyColdStarts, n); // here we inject a custom env variable 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}); + 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}); - const returnObj = { + return { initConfigurations: initConfigurations, iterator: { index: 0, @@ -50,7 +50,6 @@ module.exports.handler = async(event, context) => { }, powerValues: powerValues, }; - return returnObj; }; diff --git a/lambda/publisher.js b/lambda/publisher.js index d71ca7a..84ae18b 100644 --- a/lambda/publisher.js +++ b/lambda/publisher.js @@ -6,22 +6,8 @@ const utils = require('./utils'); module.exports.handler = async(event, context) => { const {lambdaConfigurations, currConfig, lambdaARN} = validateInputs(event); const currentIterator = lambdaConfigurations.iterator; - const aliases = lambdaConfigurations.aliases || []; - - let description; - // Alias may not exist when we are reverting the Lambda function to its original configuration - if (typeof currConfig.alias !== 'undefined'){ - description = currConfig.alias; - } else { - description = currConfig.description; - } - // publish version & assign alias (if present) - await utils.createPowerConfiguration(lambdaARN, currConfig.powerValue, currConfig.alias, description); - if (typeof currConfig.alias !== 'undefined') { - // keep track of all aliases - aliases.push(currConfig.alias); - } + await utils.createPowerConfiguration(lambdaARN, currConfig.powerValue, currConfig.alias, currConfig.description); // update iterator const updatedIterator = { @@ -29,13 +15,11 @@ module.exports.handler = async(event, context) => { count: currentIterator.count, continue: ((currentIterator.index + 1) < currentIterator.count), }; - const updatedLambdaConfigurations = { + return { initConfigurations: ((updatedIterator.continue) ? lambdaConfigurations.initConfigurations : undefined), iterator: updatedIterator, - aliases: aliases, powerValues: lambdaConfigurations.powerValues, }; - return updatedLambdaConfigurations; }; function validateInputs(event) { if (!event.lambdaARN) { From aad097cad32ea646f635e178922612143864fd96 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Thu, 16 May 2024 12:03:47 +0200 Subject: [PATCH 20/38] PR Feedback --- lambda/cleaner.js | 12 +++++++----- lambda/executor.js | 1 + lambda/optimizer.js | 3 +-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lambda/cleaner.js b/lambda/cleaner.js index ed2a1a3..e0dc99c 100644 --- a/lambda/cleaner.js +++ b/lambda/cleaner.js @@ -31,14 +31,16 @@ module.exports.handler = async(event, context) => { }; const buildAliasListForCleanup = (lambdaARN, onlyColdStarts, powerValues, num) => { - let aliases; if (onlyColdStarts){ - aliases = powerValues.map((powerValue) => utils.range(num).map((value) => utils.buildAliasString(`RAM${powerValue}`, onlyColdStarts, value))).flat(); - } else { - aliases = powerValues.map((powerValue) => utils.buildAliasString(`RAM${powerValue}`)); + return powerValues.map((powerValue) => { + return utils.range(num).map((index) => { + return utils.buildAliasString(`RAM${powerValue}`, onlyColdStarts, index); + }); + }).flat(); } - return aliases; + return powerValues.map((powerValue) => utils.buildAliasString(`RAM${powerValue}`)); }; + const extractDataFromInput = (event) => { return { lambdaARN: event.lambdaARN, diff --git a/lambda/executor.js b/lambda/executor.js index 3438cb3..3812cb9 100644 --- a/lambda/executor.js +++ b/lambda/executor.js @@ -175,6 +175,7 @@ const runInSeries = async({num, lambdaARN, lambdaAlias, payloads, preARN, postAR // run invocations in series 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 diff --git a/lambda/optimizer.js b/lambda/optimizer.js index 84af89c..c301850 100644 --- a/lambda/optimizer.js +++ b/lambda/optimizer.js @@ -26,8 +26,7 @@ module.exports.handler = async(event, context) => { await utils.setLambdaPower(lambdaARN, optimalValue); } else { // create/update alias - const {envVars} = await utils.getLambdaPower(lambdaARN); - await utils.createPowerConfiguration(lambdaARN, optimalValue, autoOptimizeAlias, envVars); + await utils.createPowerConfiguration(lambdaARN, optimalValue, autoOptimizeAlias); } return 'OK'; From 2a547a6827b1eeb2676fac1fc9e4e7e4c255e79c Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Thu, 16 May 2024 12:18:54 +0200 Subject: [PATCH 21/38] Fix unit tests and linting --- test/unit/test-lambda.js | 34 +++++++++++----------------------- test/unit/test-utils.js | 21 +++++++++++++-------- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/test/unit/test-lambda.js b/test/unit/test-lambda.js index 74adf4e..780a6f9 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,14 +115,6 @@ describe('Lambda Functions', async() => { const error = new ResourceNotFoundException('alias is not defined'); throw error; }); - sandBox.stub(utils, 'getLambdaPower') - .callsFake(async() => { - getLambdaPowerCounter++; - return { - power: 1024, - envVars: {}, - }; - }); setLambdaPowerStub = sandBox.stub(utils, 'setLambdaPower') .callsFake(async() => { setLambdaPowerCounter++; @@ -217,13 +208,13 @@ describe('Lambda Functions', async() => { expect(generatedValues.initConfigurations.length).to.be(47); // 46 power values plus the previous Lambda power configuration }); - it('should generate N aliases and versions', async() => { + 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 aliases and versions', async() => { + 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 @@ -317,15 +308,12 @@ describe('Lambda Functions', async() => { count: 1, }, }}); - expect(getLambdaPowerCounter).to.be(1); 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(originalIndex + 1); // index should be incremented by 1 expect(generatedValues.iterator.continue).to.be(false); // the iterator should be set to continue=false - expect(generatedValues.aliases.length).to.be(1); - expect(generatedValues.aliases[0]).to.be(aliasValue); }); it('should publish the version even if an alias is not specified', async() => { await invokeForSuccess(handler, { @@ -419,11 +407,11 @@ describe('Lambda Functions', async() => { let invalidEvents = [ null, {}, - { lambdaARN: null, lambdaConfigurations: { aliases: ['RAM128']}}, - { lambdaARN: '', lambdaConfigurations: { aliases: ['RAM128']}}, - { lambdaARN: false, lambdaConfigurations: { aliases: ['RAM128']}}, - { lambdaARN: 0, lambdaConfigurations: { aliases: ['RAM128']}}, - { lambdaARN: '', lambdaConfigurations: { aliases: ['RAM128']}}, + { lambdaARN: null, lambdaConfigurations: singleAliasConfig}, + { lambdaARN: '', lambdaConfigurations: singleAliasConfig}, + { lambdaARN: false, lambdaConfigurations: singleAliasConfig}, + { lambdaARN: 0, lambdaConfigurations: singleAliasConfig}, + { lambdaARN: '', lambdaConfigurations: singleAliasConfig}, ]; invalidEvents.forEach(async(event) => { @@ -435,11 +423,11 @@ describe('Lambda Functions', async() => { invalidEvents = [ { lambdaARN: 'arnOK'}, { lambdaARN: 'arnOK', lambdaConfigurations: {}}, - { lambdaARN: 'arnOK', lambdaConfigurations: { aliases: []}}, + { lambdaARN: 'arnOK', lambdaConfigurations: { powerValues: []}}, ]; invalidEvents.forEach(async(event) => { - it('should explode if invoked without valid aliases - ' + JSON.stringify(event), async() => { + it('should explode if invoked without valid powerValues - ' + JSON.stringify(event), async() => { await invokeForFailure(handler, event); }); }); @@ -466,7 +454,7 @@ describe('Lambda Functions', async() => { }); }); - const eventOK = { lambdaARN: 'arnOK', lambdaConfigurations: {aliases: ['RAM128', 'RAM256', 'RAM512'] }}; + const eventOK = { lambdaARN: 'arnOK', lambdaConfigurations: {powerValues: ['128', '256', '512'] }}; it('should invoke the given cb, when done', async() => { await invokeForSuccess(handler, eventOK); diff --git a/test/unit/test-utils.js b/test/unit/test-utils.js index b2f9d6e..acbd78b 100644 --- a/test/unit/test-utils.js +++ b/test/unit/test-utils.js @@ -38,7 +38,7 @@ lambdaMock.on(GetFunctionConfigurationCommand).resolves({ State: 'Active', LastUpdateStatus: 'Successful', Architectures: ['x86_64'], - Environment: {Variables: {TEST: 'OK'}}, + Description: 'Sample Description', }); lambdaMock.on(UpdateFunctionConfigurationCommand).resolves({}); lambdaMock.on(PublishVersionCommand).resolves({}); @@ -134,26 +134,31 @@ describe('Lambda Utils', () => { }); describe('getLambdaPower', () => { - it('should return the power value and env vars', 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.envVars).to.be.an('object'); - expect(value.envVars.TEST).to.be('OK'); + expect(value.description).to.be('Sample Description'); }); - it('should return the power value and env vars even when empty env', async() => { + 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'], - Environment: null, // this is null if no vars are set + 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.envVars).to.be.an('object'); - expect(value.envVars.TEST).to.be(undefined); + expect(value.description).to.be(''); }); }); From 4b58451223cefc8a7b05099fcb8d216d78675fe4 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Thu, 16 May 2024 12:33:48 +0200 Subject: [PATCH 22/38] remove unnecessary permission --- template.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/template.yml b/template.yml index e8794bd..62f4938 100644 --- a/template.yml +++ b/template.yml @@ -177,7 +177,6 @@ Resources: Action: - lambda:InvokeFunction - lambda:GetFunctionConfiguration - - lambda:GetFunction Resource: !Ref lambdaResource - !If - S3BucketProvided # if S3 bucket is provided From 9a9b2430d397d5f3349a26c95f22a6e24c0559a5 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Thu, 16 May 2024 12:43:04 +0200 Subject: [PATCH 23/38] remove unnecessary permission --- terraform/module/json_files/executor.json | 5 ++-- .../module/json_files/state_machine.json | 27 ++++++++++++------- terraform/module/locals.tf | 2 +- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/terraform/module/json_files/executor.json b/terraform/module/json_files/executor.json index 7ff16ca..758d7a5 100644 --- a/terraform/module/json_files/executor.json +++ b/terraform/module/json_files/executor.json @@ -5,10 +5,9 @@ "Effect": "Allow", "Action": [ "lambda:InvokeFunction", - "lambda:GetFunctionConfiguration", - "lambda:GetFunction" + "lambda:GetFunctionConfiguration" ], "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 cfbcdeb..bcb6bd8 100644 --- a/terraform/module/json_files/state_machine.json +++ b/terraform/module/json_files/state_machine.json @@ -5,15 +5,22 @@ "Initializer": { "Type": "Task", "Resource": "${initializerArn}", - "Next": "versionPublisher", - "ResultPath": "$.powerValues", - "TimeoutSeconds": 600 + "Next": "Publisher", + "ResultPath": "$.lambdaConfigurations", + "TimeoutSeconds": 600, + "Catch": [ + { + "ErrorEquals": ["States.ALL"], + "Next": "CleanUpOnError", + "ResultPath": "$.error" + } + ] }, - "versionPublisher": { + "Publisher": { "Type": "Task", - "Resource": "${versionPublisherArn}", + "Resource": "${publisherArn}", "Next": "IsCountReached", - "ResultPath": "$.powerValues", + "ResultPath": "$.lambdaConfigurations", "TimeoutSeconds": 600, "Catch": [{ "ErrorEquals": [ "States.ALL" ], @@ -25,9 +32,9 @@ "Type": "Choice", "Choices": [ { - "Variable": "$.powerValues.iterator.continue", + "Variable": "$.lambdaConfigurations.iterator.continue", "BooleanEquals": true, - "Next": "versionPublisher" + "Next": "Publisher" } ], "Default": "Branching" @@ -35,7 +42,7 @@ "Branching": { "Type": "Map", "Next": "Cleaner", - "ItemsPath": "$.powerValues.powerValues", + "ItemsPath": "$.lambdaConfigurations.powerValues", "ResultPath": "$.stats", "ItemSelector": { "input.$": "$", @@ -95,4 +102,4 @@ "End": true } } -} \ No newline at end of file +} diff --git a/terraform/module/locals.tf b/terraform/module/locals.tf index 0b148ed..0d592f1 100644 --- a/terraform/module/locals.tf +++ b/terraform/module/locals.tf @@ -11,7 +11,7 @@ locals { "${path.module}/json_files/state_machine.json", { initializerArn = aws_lambda_function.initializer.arn, - versionPublisherArn = aws_lambda_function.publisher.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, From 1979fb63a3e9a14ba0c365208927b8275f1db8fb Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Thu, 16 May 2024 13:26:57 +0200 Subject: [PATCH 24/38] fix node version in the Terraform template --- terraform/module/lambda.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terraform/module/lambda.tf b/terraform/module/lambda.tf index 960ea1f..7db7f54 100644 --- a/terraform/module/lambda.tf +++ b/terraform/module/lambda.tf @@ -161,7 +161,7 @@ resource "aws_lambda_function" "publisher" { # source_code_hash = "${base64sha256(file("lambda_function_payload.zip"))}" source_code_hash = data.archive_file.app.output_base64sha256 - runtime = "nodejs16.x" + runtime = "nodejs20.x" dynamic "vpc_config" { for_each = var.vpc_subnet_ids != null && var.vpc_security_group_ids != null ? [true] : [] From 446fff2e3d7b2af07fae370254df81ed80cd541b Mon Sep 17 00:00:00 2001 From: Alex Casalboni Date: Mon, 20 May 2024 15:25:32 +0200 Subject: [PATCH 25/38] remove error catch from initializer state --- statemachine/statemachine.asl.json | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/statemachine/statemachine.asl.json b/statemachine/statemachine.asl.json index 79d9110..c2c23a1 100644 --- a/statemachine/statemachine.asl.json +++ b/statemachine/statemachine.asl.json @@ -7,14 +7,7 @@ "Resource": "${initializerArn}", "Next": "Publisher", "ResultPath": "$.lambdaConfigurations", - "TimeoutSeconds": ${totalExecutionTimeout}, - "Catch": [ - { - "ErrorEquals": ["States.ALL"], - "Next": "CleanUpOnError", - "ResultPath": "$.error" - } - ] + "TimeoutSeconds": ${totalExecutionTimeout} }, "Publisher": { "Type": "Task", From e8b7d176f2b6fd607a4cd115a10d787d5e1e4f71 Mon Sep 17 00:00:00 2001 From: Alex Casalboni Date: Mon, 20 May 2024 15:26:28 +0200 Subject: [PATCH 26/38] Uupdate analyzer and executor to include init stats and new state machine transitions cost --- lambda/analyzer.js | 4 +++- lambda/executor.js | 12 ++++++++-- lambda/utils.js | 59 +++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 66 insertions(+), 9 deletions(-) diff --git a/lambda/analyzer.js b/lambda/analyzer.js index 78e9d35..d289055 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/executor.js b/lambda/executor.js index 3812cb9..591c1b6 100644 --- a/lambda/executor.js +++ b/lambda/executor.js @@ -74,7 +74,7 @@ module.exports.handler = async(event, context) => { // get base cost for Lambda const baseCost = utils.lambdaBaseCost(utils.regionFromARN(lambdaARN), architecture); - return computeStatistics(baseCost, results, value, discardTopBottom); + return computeStatistics(baseCost, results, value, discardTopBottom, onlyColdStarts); }; const validateInput = (lambdaARN, value, num) => { @@ -191,10 +191,18 @@ const runInSeries = async({num, lambdaARN, lambdaAlias, payloads, preARN, postAR return results; }; -const computeStatistics = (baseCost, results, value, discardTopBottom) => { +const computeStatistics = (baseCost, results, value, discardTopBottom, onlyColdStarts) => { // use results (which include logs) to compute average duration ... const durations = utils.parseLogAndExtractDurations(results); + if (onlyColdStarts) { + // we care about the init duration in this case + const initDurations = utils.parseLogAndExtractInitDurations(results); + // add init duration to total duration + initDurations.forEach((value, index) => { + durations[index] += value; + }); + } const averageDuration = utils.computeAverageDuration(durations, discardTopBottom); console.log('Average duration: ', averageDuration); diff --git a/lambda/utils.js b/lambda/utils.js index 5688f86..5f6c330 100644 --- a/lambda/utils.js +++ b/lambda/utils.js @@ -14,7 +14,25 @@ const url = require('url'); const utils = module.exports; // 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); @@ -511,6 +529,13 @@ module.exports.parseLogAndExtractDurations = (data) => { }); }; +module.exports.parseLogAndExtractInitDurations = (data) => { + return data.map(log => { + const logString = utils.base64decode(log.LogResult || ''); + return utils.extractInitDuration(logString); + }); +}; + /** * Compute total cost */ @@ -573,18 +598,22 @@ module.exports.extractDuration = (log) => { /** * Extract duration (in ms) from a given text log. */ -module.exports.extractDurationFromText = (log) => { - const regex = /\tBilled Duration: (\d+) ms/m; +module.exports.extractDurationFromText = (log, init=false) => { + let regex = /\tBilled Duration: (\d+) ms/m; + if (init) { + regex = /\tInit Duration: (\d+\.\d+) ms/m; + } + const match = regex.exec(log); 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). */ -module.exports.extractDurationFromJSON = (log) => { +module.exports.extractDurationFromJSON = (log, init=false) => { // extract each line and parse it to JSON object const lines = log.split('\n').filter((line) => line.startsWith('{')).map((line) => { try { @@ -597,12 +626,30 @@ 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 = 'billedDurationMs'; + if (init) { + field = 'initDurationMs'; + } + return durationLine.record.metrics[field]; } throw new Error('Unrecognized JSON log'); }; +/** + * Extract init duration (in ms) from a given Lambda's CloudWatch log. + */ +module.exports.extractInitDuration = (log) => { + if (log.charAt(0) === '{') { + // extract from JSON (multi-line) + return utils.extractDurationFromJSON(log, true); + } else { + // extract from text + return utils.extractDurationFromText(log, true); + } +}; + + /** * Encode a given string to base64. */ From 71aa0cb337d40fe78d85b185c1b7f0b8983b8898 Mon Sep 17 00:00:00 2001 From: Alex Casalboni Date: Mon, 20 May 2024 15:26:43 +0200 Subject: [PATCH 27/38] update tests and coverage --- test/unit/test-lambda.js | 39 ++++++++++- test/unit/test-utils.js | 144 ++++++++++++++++++++++++++++++--------- 2 files changed, 147 insertions(+), 36 deletions(-) diff --git a/test/unit/test-lambda.js b/test/unit/test-lambda.js index 780a6f9..c49680f 100644 --- a/test/unit/test-lambda.js +++ b/test/unit/test-lambda.js @@ -315,6 +315,7 @@ describe('Lambda Functions', async() => { expect(generatedValues.iterator.index).to.be(originalIndex + 1); // index should be incremented by 1 expect(generatedValues.iterator.continue).to.be(false); // the iterator should be set to continue=false }); + it('should publish the version even if an alias is not specified', async() => { await invokeForSuccess(handler, { lambdaARN: 'arnOK', @@ -328,6 +329,7 @@ describe('Lambda Functions', async() => { }, }}); }); + it('should update an alias if it already exists', async() => { getLambdaAliasStub && getLambdaAliasStub.restore(); getLambdaAliasStub = sandBox.stub(utils, 'getLambdaAlias') @@ -377,6 +379,26 @@ describe('Lambda Functions', async() => { 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') @@ -454,7 +476,11 @@ describe('Lambda Functions', async() => { }); }); - const eventOK = { lambdaARN: 'arnOK', lambdaConfigurations: {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); @@ -490,6 +516,15 @@ 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, + }); + }); + }); describe('executor', () => { @@ -1556,7 +1591,7 @@ describe('Lambda Functions', async() => { console.log('response', response); - expect(response.averageDuration).to.be(27.7); + expect(response.averageDuration).to.be(27.71); }); it('should waitForAliasActive for each Alias when onlyColdStarts is set', async() => { diff --git a/test/unit/test-utils.js b/test/unit/test-utils.js index acbd78b..c38ac82 100644 --- a/test/unit/test-utils.js +++ b/test/unit/test-utils.js @@ -107,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'; @@ -122,13 +122,21 @@ 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); }); }); @@ -202,36 +210,40 @@ 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' + ; + + // 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 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); @@ -256,12 +268,41 @@ describe('Lambda Utils', () => { }); 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(); }); }); + describe('extractInitDuration', () => { + + it('should extract the init duration from a Lambda log (text format)', () => { + expect(utils.extractInitDuration(textLog)).to.be(100.99); + }); + + it('should return 0 if init duration is not found', () => { + expect(utils.extractInitDuration('hello world')).to.be(0); + const partialLog = 'START RequestId: 55bc566d-1e2c-11e7-93e6-6705ceb4c1cc Version: $LATEST\n'; + expect(utils.extractInitDuration(partialLog)).to.be(0); + }); + + it('should extract the init duration from a Lambda log (json format)', () => { + expect(utils.extractInitDuration(jsonLog)).to.be(10); + }); + + it('should extract the init duration from a Lambda log (json text mixed format)', () => { + expect(utils.extractInitDuration(jsonMixedLog)).to.be(20); + }); + + it('should extract the init duration from a Lambda log (json text mixed format with invalid JSON)', () => { + expect(utils.extractInitDuration(jsonMixedLogWithInvalidJSON)).to.be(30); + }); + + it('should explode if invalid json format document is provided', () => { + expect(() => utils.extractInitDuration(invalidJSONLog)).to.throwError(); + }); + + }); + describe('computePrice', () => { const minCost = 2.1e-9; // $ per ms const minRAM = 128; // MB @@ -310,6 +351,41 @@ describe('Lambda Utils', () => { }); }); + describe('parseLogAndExtractInitDurations', () => { + const results = [ + // 300.00 ms + { StatusCode: 200, LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMS4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMSBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCUluaXQgRHVyYXRpb246IDMwMC4wMCBtcw==', Payload: 'null' }, + // 100.99ms + { StatusCode: 200, LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMS4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMSBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCUluaXQgRHVyYXRpb246IDEwMC45OSBtcw==', Payload: 'null' }, + // 500.55 ms + { StatusCode: 200, LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMS4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMSBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCUluaXQgRHVyYXRpb246IDUwMC41NSBtcw==', Payload: 'null' }, + // 700.77 ms + { StatusCode: 200, LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMS4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMSBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCUluaXQgRHVyYXRpb246IDcwMC43NyBtcw==', Payload: 'null' }, + // 900.50 ms + { StatusCode: 200, LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMS4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMSBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCUluaXQgRHVyYXRpb246IDkwMC41MCBtcw==', Payload: 'null' }, + ]; + + it('should return the list of init durations', () => { + const durations = utils.parseLogAndExtractInitDurations(results); + expect(durations).to.be.a('array'); + expect(durations.length).to.be(5); + expect(durations).to.eql([300.00, 100.99, 500.55, 700.77, 900.50]); + }); + it('should return empty list if empty results', () => { + const durations = utils.parseLogAndExtractInitDurations([]); + expect(durations).to.be.an('array'); + expect(durations.length).to.be(0); + }); + + it('should not explode if missing logs', () => { + const durations = utils.parseLogAndExtractInitDurations([ + { StatusCode: 200, Payload: 'null' }, + ]); + expect(durations).to.be.an('array'); + expect(durations).to.eql([0]); + }); + }); + describe('computeAverageDuration', () => { const durations = [ // keep 5 values because it's the minimum length From 40c221badd852b799b33588691889c4b690ad0d6 Mon Sep 17 00:00:00 2001 From: Alex Casalboni Date: Mon, 20 May 2024 15:27:42 +0200 Subject: [PATCH 28/38] linting --- lambda/utils.js | 6 +++--- test/unit/test-lambda.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lambda/utils.js b/lambda/utils.js index 5f6c330..8eebe17 100644 --- a/lambda/utils.js +++ b/lambda/utils.js @@ -32,7 +32,7 @@ module.exports.stepFunctionsCost = (nPower, onlyColdStarts, num) => { } return +(baseCostPerTransition * multiplier).toFixed(5); -} +}; module.exports.stepFunctionsBaseCost = () => { const prices = JSON.parse(process.env.sfCosts); @@ -598,7 +598,7 @@ module.exports.extractDuration = (log) => { /** * Extract duration (in ms) from a given text log. */ -module.exports.extractDurationFromText = (log, init=false) => { +module.exports.extractDurationFromText = (log, init = false) => { let regex = /\tBilled Duration: (\d+) ms/m; if (init) { regex = /\tInit Duration: (\d+\.\d+) ms/m; @@ -613,7 +613,7 @@ module.exports.extractDurationFromText = (log, init=false) => { /** * Extract duration (in ms) from a given JSON log (multi-line). */ -module.exports.extractDurationFromJSON = (log, init=false) => { +module.exports.extractDurationFromJSON = (log, init = false) => { // extract each line and parse it to JSON object const lines = log.split('\n').filter((line) => line.startsWith('{')).map((line) => { try { diff --git a/test/unit/test-lambda.js b/test/unit/test-lambda.js index c49680f..75b60bf 100644 --- a/test/unit/test-lambda.js +++ b/test/unit/test-lambda.js @@ -477,7 +477,7 @@ describe('Lambda Functions', async() => { }); const eventOK = { - num:10, + num: 10, lambdaARN: 'arnOK', lambdaConfigurations: {powerValues: ['128', '256', '512'] }, }; From 6f001dacfb0e6fa942c282c7981afdd8d911e8b0 Mon Sep 17 00:00:00 2001 From: Alex Casalboni Date: Mon, 20 May 2024 16:00:12 +0200 Subject: [PATCH 29/38] Improve publisher coverage & linting --- lambda/publisher.js | 23 +++++++++++------- test/unit/test-lambda.js | 52 +++++++++++++++++++++++++++++++--------- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/lambda/publisher.js b/lambda/publisher.js index 84ae18b..f1743cf 100644 --- a/lambda/publisher.js +++ b/lambda/publisher.js @@ -9,17 +9,22 @@ module.exports.handler = async(event, context) => { // publish version & assign alias (if present) await utils.createPowerConfiguration(lambdaARN, currConfig.powerValue, currConfig.alias, currConfig.description); - // update iterator - const updatedIterator = { - index: (currentIterator.index + 1), - count: currentIterator.count, - continue: ((currentIterator.index + 1) < currentIterator.count), - }; - return { - initConfigurations: ((updatedIterator.continue) ? lambdaConfigurations.initConfigurations : undefined), - iterator: updatedIterator, + 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) { diff --git a/test/unit/test-lambda.js b/test/unit/test-lambda.js index 75b60bf..59521d5 100644 --- a/test/unit/test-lambda.js +++ b/test/unit/test-lambda.js @@ -238,7 +238,8 @@ describe('Lambda Functions', async() => { { }, {lambdaARN: 'arnOK'}, {lambdaARN: 'arnOK', lambdaConfigurations: {}}, - {lambdaARN: 'arnOK', + { + lambdaARN: 'arnOK', lambdaConfigurations: { initConfigurations: [{ powerValue: 512, @@ -246,7 +247,8 @@ describe('Lambda Functions', async() => { }], }, }, - {lambdaARN: 'arnOK', + { + lambdaARN: 'arnOK', lambdaConfigurations: { iterator: { index: 1, @@ -254,7 +256,8 @@ describe('Lambda Functions', async() => { }, }, }, - {lambdaARN: 'arnOK', + { + lambdaARN: 'arnOK', lambdaConfigurations: { initConfigurations: [{ powerValue: 512, @@ -269,7 +272,8 @@ describe('Lambda Functions', async() => { }, }, }, - {lambdaARN: 'arnOK', + { + lambdaARN: 'arnOK', lambdaConfigurations: { initConfigurations: [{ powerValue: 512, @@ -292,28 +296,54 @@ describe('Lambda Functions', async() => { }); }); - it('should publish the given lambda version', async() => { + 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 + }); - const aliasValue = 'RAM512'; - const originalIndex = 0; + it('should publish the given lambda version (last iteration)', async() => { const generatedValues = await invokeForSuccess(handler, { lambdaARN: 'arnOK', lambdaConfigurations: { initConfigurations: [{ powerValue: 512, - alias: aliasValue, + alias: 'RAM512', + }, { + powerValue: 1024, + alias: 'RAM1024', }], iterator: { - index: originalIndex, - count: 1, + 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(originalIndex + 1); // index should be incremented by 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 }); it('should publish the version even if an alias is not specified', async() => { From 940ecd890ba9d0cb2e80a00eb895ed68c8980d77 Mon Sep 17 00:00:00 2001 From: Alex Casalboni Date: Mon, 20 May 2024 16:30:25 +0200 Subject: [PATCH 30/38] Update documentation for onlyColdStarts --- README-ADVANCED.md | 16 +++--- README.md | 79 +++++++++++++++--------------- imgs/state-machine-screenshot.png | Bin 40359 -> 198624 bytes 3 files changed, 49 insertions(+), 46 deletions(-) diff --git a/README-ADVANCED.md b/README-ADVANCED.md index 06b847a..aee7398 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 b6df866..ff5dcaf 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 bc1eda0930028c9e819bc035a9b05ad7f1ff61cf..1ed27b993948ee0d06253e568f92663c158c61cd 100644 GIT binary patch literal 198624 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%0AEUyu8VpK!+beP5>*M$KW%U+ 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 literal 40359 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 R@cAjoJWp3Ymvv4FO#sfx#qR(B From 000cd74cc8e7b6767436a5a47171a36fa2627f44 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Thu, 23 May 2024 13:34:25 +0200 Subject: [PATCH 31/38] PR feedback --- lambda/cleaner.js | 2 +- lambda/initializer.js | 4 ++-- lambda/utils.js | 1 + test/unit/test-lambda.js | 36 ++++++++++++++++++++++++++++++++++++ test/unit/test-utils.js | 16 ++++++++++++++++ 5 files changed, 56 insertions(+), 3 deletions(-) diff --git a/lambda/cleaner.js b/lambda/cleaner.js index e0dc99c..89b7145 100644 --- a/lambda/cleaner.js +++ b/lambda/cleaner.js @@ -46,7 +46,7 @@ const extractDataFromInput = (event) => { lambdaARN: event.lambdaARN, powerValues: event.lambdaConfigurations.powerValues, onlyColdStarts: event.onlyColdStarts, - num: parseInt(event.num, 10), // use the default in case it was not defined + num: parseInt(event.num, 10), // parse as we do in the initializer }; }; diff --git a/lambda/initializer.js b/lambda/initializer.js index 73db502..56e422b 100644 --- a/lambda/initializer.js +++ b/lambda/initializer.js @@ -28,11 +28,11 @@ module.exports.handler = async(event, context) => { for (let powerValue of powerValues){ const baseAlias = 'RAM' + powerValue; if (!onlyColdStarts){ - initConfigurations.push({powerValue: powerValue, alias: baseAlias, description: `${description} - ${baseAlias}`}); + 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 env variable to force the creation of a new version + // 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}`}); } diff --git a/lambda/utils.js b/lambda/utils.js index 8eebe17..4151d94 100644 --- a/lambda/utils.js +++ b/lambda/utils.js @@ -110,6 +110,7 @@ module.exports.createPowerConfiguration = async(lambdaARN, value, alias, descrip 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; diff --git a/test/unit/test-lambda.js b/test/unit/test-lambda.js index 59521d5..8c03855 100644 --- a/test/unit/test-lambda.js +++ b/test/unit/test-lambda.js @@ -555,6 +555,42 @@ describe('Lambda Functions', async() => { }); }); + 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', () => { diff --git a/test/unit/test-utils.js b/test/unit/test-utils.js index c38ac82..e208572 100644 --- a/test/unit/test-utils.js +++ b/test/unit/test-utils.js @@ -1250,4 +1250,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'); + }); + }); }); From 797f89c855e636e9b21857dc5221c1b5347f72cf Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Thu, 23 May 2024 14:47:22 +0200 Subject: [PATCH 32/38] Remove getAlias permission from the Initializer --- template.yml | 1 - terraform/module/json_files/initializer.json | 1 - 2 files changed, 2 deletions(-) diff --git a/template.yml b/template.yml index 62f4938..cfe6733 100644 --- a/template.yml +++ b/template.yml @@ -131,7 +131,6 @@ Resources: Statement: - Effect: Allow Action: - - lambda:GetAlias - lambda:GetFunctionConfiguration Resource: !Ref lambdaResource publisher: diff --git a/terraform/module/json_files/initializer.json b/terraform/module/json_files/initializer.json index f202fa9..f236573 100644 --- a/terraform/module/json_files/initializer.json +++ b/terraform/module/json_files/initializer.json @@ -4,7 +4,6 @@ { "Effect": "Allow", "Action": [ - "lambda:GetAlias", "lambda:GetFunctionConfiguration" ], "Resource": "arn:aws:lambda:*:${account_id}:function:*" From 355ea6fa4e65fd59d8c0102801e098baa47d2ec6 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Thu, 23 May 2024 14:56:14 +0200 Subject: [PATCH 33/38] PR Feedback --- lambda/utils.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lambda/utils.js b/lambda/utils.js index 4151d94..075d828 100644 --- a/lambda/utils.js +++ b/lambda/utils.js @@ -217,6 +217,8 @@ module.exports.setLambdaPower = (lambdaARN, value, description) => { 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); From 18a8a65677b14208b97efc23c2da28e9ad5f65b2 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Fri, 24 May 2024 17:39:27 +0200 Subject: [PATCH 34/38] Modify logic for computing duration and cost --- lambda/executor.js | 29 ++++++--------- lambda/utils.js | 77 ++++++++++++++++++++++------------------ test/unit/test-lambda.js | 8 ++--- 3 files changed, 58 insertions(+), 56 deletions(-) diff --git a/lambda/executor.js b/lambda/executor.js index 591c1b6..9b7034b 100644 --- a/lambda/executor.js +++ b/lambda/executor.js @@ -74,7 +74,7 @@ module.exports.handler = async(event, context) => { // get base cost for Lambda const baseCost = utils.lambdaBaseCost(utils.regionFromARN(lambdaARN), architecture); - return computeStatistics(baseCost, results, value, discardTopBottom, onlyColdStarts); + return computeStatistics(baseCost, results, value, discardTopBottom); }; const validateInput = (lambdaARN, value, num) => { @@ -191,27 +191,20 @@ const runInSeries = async({num, lambdaARN, lambdaAlias, payloads, preARN, postAR return results; }; -const computeStatistics = (baseCost, results, value, discardTopBottom, onlyColdStarts) => { - // use results (which include logs) to compute average duration ... - - const durations = utils.parseLogAndExtractDurations(results); - if (onlyColdStarts) { - // we care about the init duration in this case - const initDurations = utils.parseLogAndExtractInitDurations(results); - // add init duration to total duration - initDurations.forEach((value, index) => { - durations[index] += value; - }); - } +const computeStatistics = (baseCost, results, value, discardTopBottom) => { - 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/utils.js b/lambda/utils.js index 075d828..c5d77d5 100644 --- a/lambda/utils.js +++ b/lambda/utils.js @@ -528,14 +528,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, 'durationMs') + + utils.extractDuration(logString, 'initDurationMs') + + utils.extractDuration(logString, 'restoreDurationMs'); }); }; - -module.exports.parseLogAndExtractInitDurations = (data) => { +module.exports.parseLogAndExtractBilledDurations = (data) => { return data.map(log => { const logString = utils.base64decode(log.LogResult || ''); - return utils.extractInitDuration(logString); + // 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, 'billedDurationMs') + + utils.extractDuration(logString, 'billedRestoreDurationMs'); }); }; @@ -588,35 +598,49 @@ 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 (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 'billedDurationMs': + return /\tBilled Duration: (\d+) ms/m; + case 'initDurationMs': + return /\tInit Duration: (\d+\.\d+) ms/m; + case 'durationMs': + return /\tDuration: (\d+\.\d+) ms/m; + case 'restoreDurationMs': + return /\tRestore Duration: (\d+\.\d+) ms/m; + case 'billedRestoreDurationMs': + return /\tBilled Restore Duration: (\d+\.\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, init = false) => { - let regex = /\tBilled Duration: (\d+) ms/m; - if (init) { - regex = /\tInit Duration: (\d+\.\d+) ms/m; - } +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 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, init = false) => { +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 { @@ -629,29 +653,14 @@ module.exports.extractDurationFromJSON = (log, init = false) => { // find the log corresponding to the invocation report const durationLine = lines.find((line) => line.type === 'platform.report'); if (durationLine){ - let field = 'billedDurationMs'; - if (init) { - field = 'initDurationMs'; - } - return durationLine.record.metrics[field]; + 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'); }; -/** - * Extract init duration (in ms) from a given Lambda's CloudWatch log. - */ -module.exports.extractInitDuration = (log) => { - if (log.charAt(0) === '{') { - // extract from JSON (multi-line) - return utils.extractDurationFromJSON(log, true); - } else { - // extract from text - return utils.extractDurationFromText(log, true); - } -}; - /** * Encode a given string to base64. diff --git a/test/unit/test-lambda.js b/test/unit/test-lambda.js index 8c03855..f8aceb7 100644 --- a/test/unit/test-lambda.js +++ b/test/unit/test-lambda.js @@ -1539,11 +1539,11 @@ describe('Lambda Functions', async() => { 3.5, 3.5, 4.333333333333333, - 27.7, + 27.72, ]; const logResults = [ - // Duration 0.1ms - Billed 1ms + // Duration 0.1ms - Init Duration 0.1ms - Billed 1ms { StatusCode: 200, LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMC4xIG1zCUJpbGxlZCBEdXJhdGlvbjogMSBtcwlJbml0IER1cmF0aW9uOiAwLjEgbXMgCU1lbW9yeSBTaXplOiAxMjggTUIJTWF4IE1lbW9yeSBVc2VkOiAxNSBNQgkK', @@ -1558,7 +1558,7 @@ describe('Lambda Functions', async() => { // Duration 2.0ms - Billed 2ms { StatusCode: 200, - LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMi4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMm1zIAlNZW1vcnkgU2l6ZTogMTI4IE1CCU1heCBNZW1vcnkgVXNlZDogMTUgTUIJ', + LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkgMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQgMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQgMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQgRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkIFJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMi4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMiBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCQ==', Payload: 'null', }, // Duration 3.0ms - Billed 3ms @@ -1657,7 +1657,7 @@ describe('Lambda Functions', async() => { console.log('response', response); - expect(response.averageDuration).to.be(27.71); + expect(response.averageDuration).to.be(27.72); }); it('should waitForAliasActive for each Alias when onlyColdStarts is set', async() => { From aba4d634e1541c7f216f5e720c77c78659f5e997 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Fri, 24 May 2024 18:00:23 +0200 Subject: [PATCH 35/38] Remove tests for deleted code Fix broken tests based on new logic --- lambda/utils.js | 3 ++ test/unit/test-lambda.js | 2 +- test/unit/test-utils.js | 73 +++------------------------------------- 3 files changed, 9 insertions(+), 69 deletions(-) diff --git a/lambda/utils.js b/lambda/utils.js index c5d77d5..7144bda 100644 --- a/lambda/utils.js +++ b/lambda/utils.js @@ -599,6 +599,9 @@ module.exports.computeAverageDuration = (durations, discardTopBottom) => { * Extract duration (in ms) from a given Lambda's CloudWatch log. */ module.exports.extractDuration = (log, durationType) => { + if (!durationType){ + durationType = 'durationMs'; // default to `durationMs` + } if (log.charAt(0) === '{') { // extract from JSON (multi-line) return utils.extractDurationFromJSON(log, durationType); diff --git a/test/unit/test-lambda.js b/test/unit/test-lambda.js index f8aceb7..9d29f26 100644 --- a/test/unit/test-lambda.js +++ b/test/unit/test-lambda.js @@ -1538,7 +1538,7 @@ describe('Lambda Functions', async() => { const trimmedDurationsValues = [ 3.5, 3.5, - 4.333333333333333, + 4.416666666666667, 27.72, ]; diff --git a/test/unit/test-utils.js b/test/unit/test-utils.js index e208572..49ba0e7 100644 --- a/test/unit/test-utils.js +++ b/test/unit/test-utils.js @@ -246,7 +246,7 @@ describe('Lambda Utils', () => { describe('extractDuration', () => { 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 return 0 if duration is not found', () => { @@ -256,50 +256,22 @@ describe('Lambda Utils', () => { }); it('should extract the duration from a Lambda log (json format)', () => { - expect(utils.extractDuration(jsonLog)).to.be(2); + expect(utils.extractDuration(jsonLog)).to.be(1.317); }); 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', () => { expect(() => utils.extractDuration(invalidJSONLog)).to.throwError(); }); - }); - - describe('extractInitDuration', () => { - - it('should extract the init duration from a Lambda log (text format)', () => { - expect(utils.extractInitDuration(textLog)).to.be(100.99); - }); - - it('should return 0 if init duration is not found', () => { - expect(utils.extractInitDuration('hello world')).to.be(0); - const partialLog = 'START RequestId: 55bc566d-1e2c-11e7-93e6-6705ceb4c1cc Version: $LATEST\n'; - expect(utils.extractInitDuration(partialLog)).to.be(0); - }); - - it('should extract the init duration from a Lambda log (json format)', () => { - expect(utils.extractInitDuration(jsonLog)).to.be(10); - }); - - it('should extract the init duration from a Lambda log (json text mixed format)', () => { - expect(utils.extractInitDuration(jsonMixedLog)).to.be(20); - }); - - it('should extract the init duration from a Lambda log (json text mixed format with invalid JSON)', () => { - expect(utils.extractInitDuration(jsonMixedLogWithInvalidJSON)).to.be(30); - }); - - it('should explode if invalid json format document is provided', () => { - expect(() => utils.extractInitDuration(invalidJSONLog)).to.throwError(); - }); + // TODO add tests to validate the totalDuration and billedDuration }); @@ -351,41 +323,6 @@ describe('Lambda Utils', () => { }); }); - describe('parseLogAndExtractInitDurations', () => { - const results = [ - // 300.00 ms - { StatusCode: 200, LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMS4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMSBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCUluaXQgRHVyYXRpb246IDMwMC4wMCBtcw==', Payload: 'null' }, - // 100.99ms - { StatusCode: 200, LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMS4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMSBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCUluaXQgRHVyYXRpb246IDEwMC45OSBtcw==', Payload: 'null' }, - // 500.55 ms - { StatusCode: 200, LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMS4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMSBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCUluaXQgRHVyYXRpb246IDUwMC41NSBtcw==', Payload: 'null' }, - // 700.77 ms - { StatusCode: 200, LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMS4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMSBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCUluaXQgRHVyYXRpb246IDcwMC43NyBtcw==', Payload: 'null' }, - // 900.50 ms - { StatusCode: 200, LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMS4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMSBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCUluaXQgRHVyYXRpb246IDkwMC41MCBtcw==', Payload: 'null' }, - ]; - - it('should return the list of init durations', () => { - const durations = utils.parseLogAndExtractInitDurations(results); - expect(durations).to.be.a('array'); - expect(durations.length).to.be(5); - expect(durations).to.eql([300.00, 100.99, 500.55, 700.77, 900.50]); - }); - it('should return empty list if empty results', () => { - const durations = utils.parseLogAndExtractInitDurations([]); - expect(durations).to.be.an('array'); - expect(durations.length).to.be(0); - }); - - it('should not explode if missing logs', () => { - const durations = utils.parseLogAndExtractInitDurations([ - { StatusCode: 200, Payload: 'null' }, - ]); - expect(durations).to.be.an('array'); - expect(durations).to.eql([0]); - }); - }); - describe('computeAverageDuration', () => { const durations = [ // keep 5 values because it's the minimum length From 103786c3b7c40a9ae124c5f83e0a94136bfa8018 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Fri, 24 May 2024 18:07:27 +0200 Subject: [PATCH 36/38] Add few TODOs as reminders --- test/unit/test-utils.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/unit/test-utils.js b/test/unit/test-utils.js index 49ba0e7..10a6525 100644 --- a/test/unit/test-utils.js +++ b/test/unit/test-utils.js @@ -321,8 +321,13 @@ describe('Lambda Utils', () => { expect(durations).to.be.an('array'); expect(durations).to.eql([0]); }); + + // TODO add test to verify it adds initDuration + duration + // TODO add test to verify it adds restoreDuration + duration }); + // TODO add tests for parseLogAndExtractBilledDurations + describe('computeAverageDuration', () => { const durations = [ // keep 5 values because it's the minimum length From 38f5c962e1775a8323d60ea76bb80d681fe49e97 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Mon, 27 May 2024 15:21:26 +0200 Subject: [PATCH 37/38] Add tests for extractDuration --- lambda/utils.js | 33 ++++++++++++-------- test/unit/test-utils.js | 68 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 85 insertions(+), 16 deletions(-) diff --git a/lambda/utils.js b/lambda/utils.js index 7144bda..745c58d 100644 --- a/lambda/utils.js +++ b/lambda/utils.js @@ -13,6 +13,15 @@ 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) /** * Computes the cost for all state transitions in this state machine execution @@ -532,9 +541,9 @@ module.exports.parseLogAndExtractDurations = (data) => { // 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, 'durationMs') + - utils.extractDuration(logString, 'initDurationMs') + - utils.extractDuration(logString, 'restoreDurationMs'); + return utils.extractDuration(logString, DURATIONS.durationMs) + + utils.extractDuration(logString, DURATIONS.initDurationMs) + + utils.extractDuration(logString, DURATIONS.restoreDurationMs); }); }; module.exports.parseLogAndExtractBilledDurations = (data) => { @@ -544,8 +553,8 @@ module.exports.parseLogAndExtractBilledDurations = (data) => { // 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, 'billedDurationMs') + - utils.extractDuration(logString, 'billedRestoreDurationMs'); + return utils.extractDuration(logString, DURATIONS.billedDurationMs) + + utils.extractDuration(logString, DURATIONS.billedRestoreDurationMs); }); }; @@ -600,7 +609,7 @@ module.exports.computeAverageDuration = (durations, discardTopBottom) => { */ module.exports.extractDuration = (log, durationType) => { if (!durationType){ - durationType = 'durationMs'; // default to `durationMs` + durationType = DURATIONS.durationMs; // default to `durationMs` } if (log.charAt(0) === '{') { // extract from JSON (multi-line) @@ -613,16 +622,16 @@ module.exports.extractDuration = (log, durationType) => { function getRegex(durationType) { switch (durationType) { - case 'billedDurationMs': + case DURATIONS.billedDurationMs: return /\tBilled Duration: (\d+) ms/m; - case 'initDurationMs': + case DURATIONS.initDurationMs: return /\tInit Duration: (\d+\.\d+) ms/m; - case 'durationMs': + case DURATIONS.durationMs: return /\tDuration: (\d+\.\d+) ms/m; - case 'restoreDurationMs': + case DURATIONS.restoreDurationMs: return /\tRestore Duration: (\d+\.\d+) ms/m; - case 'billedRestoreDurationMs': - return /\tBilled Restore Duration: (\d+\.\d+) ms/m; + case DURATIONS.billedRestoreDurationMs: + return /\tBilled Restore Duration: (\d+) ms/m; default: throw new Error(`Unknown duration type: ${durationType}`); } diff --git a/test/unit/test-utils.js b/test/unit/test-utils.js index 10a6525..d6a59a8 100644 --- a/test/unit/test-utils.js +++ b/test/unit/test-utils.js @@ -215,6 +215,11 @@ describe('Lambda Utils', () => { '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 = @@ -224,6 +229,13 @@ describe('Lambda Utils', () => { '{"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.' + @@ -242,21 +254,71 @@ describe('Lambda Utils', () => { const invalidJSONLog = '{"timestamp":"2024-02-09T08:42:44.078Z","level":"INFO","requestId":"d661f7cf-9208-46b9-85b0-213b04a91065","message":"Just some logs here =)"}'; - describe('extractDuration', () => { it('should extract the duration from a Lambda log (text format)', () => { 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', () => { expect(utils.extractDuration('hello world')).to.be(0); const partialLog = 'START RequestId: 55bc566d-1e2c-11e7-93e6-6705ceb4c1cc Version: $LATEST\n'; 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(1.317); + 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)', () => { @@ -271,8 +333,6 @@ describe('Lambda Utils', () => { expect(() => utils.extractDuration(invalidJSONLog)).to.throwError(); }); - // TODO add tests to validate the totalDuration and billedDuration - }); describe('computePrice', () => { From d837b53fc623ecd4844579abb12ddd176144d87d Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Mon, 27 May 2024 16:35:50 +0200 Subject: [PATCH 38/38] Add more tests --- test/unit/test-utils.js | 85 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 77 insertions(+), 8 deletions(-) diff --git a/test/unit/test-utils.js b/test/unit/test-utils.js index d6a59a8..f70c19a 100644 --- a/test/unit/test-utils.js +++ b/test/unit/test-utils.js @@ -350,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' }, ]; @@ -382,11 +382,80 @@ describe('Lambda Utils', () => { expect(durations).to.eql([0]); }); - // TODO add test to verify it adds initDuration + duration - // TODO add test to verify it adds restoreDuration + duration + 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); + }); - // TODO add tests for parseLogAndExtractBilledDurations + 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', () => { const durations = [