Skip to content

Commit cea8f07

Browse files
Merge pull request #206 from mriccia/onlycoldstarts
Add parameter to power-tune only cold starts
2 parents 6b61a84 + d837b53 commit cea8f07

21 files changed

+1121
-267
lines changed

README-ADVANCED.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,15 @@ There are three main costs associated with AWS Lambda Power Tuning:
5151

5252
The AWS Step Functions state machine is composed of five Lambda functions:
5353

54-
* **initializer**: create N versions and aliases corresponding to the power values provided as input (e.g. 128MB, 256MB, etc.)
55-
* **executor**: execute the given Lambda function `num` times, extract execution time from logs, and compute average cost per invocation
56-
* **cleaner**: delete all the previously generated aliases and versions
57-
* **analyzer**: compute the optimal power value (current logic: lowest average cost per invocation)
58-
* **optimizer**: automatically set the power to its optimal value (only if `autoOptimize` is `true`)
59-
60-
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`.
54+
* **Initializer**: define all the versions and aliases that need to be created (see Publisher below)
55+
* **Publisher**: create a new version and aliases corresponding to one of the power values provided as input (e.g. 128MB, 256MB, etc.)
56+
* **IsCountReached**: go back to Publisher until all the versiona and aliases have been created
57+
* **Executor**: execute the given Lambda function `num` times, extract execution time from logs, and compute average cost per invocation
58+
* **Cleaner**: delete all the previously generated aliases and versions
59+
* **Analyzer**: compute the optimal power value (current logic: lowest average cost per invocation)
60+
* **Optimizer**: automatically set the power to its optimal value (only if `autoOptimize` is `true`)
61+
62+
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`.
6163

6264
## Weighted Payloads
6365

README.md

Lines changed: 40 additions & 39 deletions
Large diffs are not rendered by default.

imgs/state-machine-screenshot.png

155 KB
Loading

lambda/analyzer.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,12 @@ const findOptimalConfiguration = (event) => {
6161
const balancedWeight = getBalancedWeight(event);
6262
const optimizationFunction = optimizationStrategies[strategy]();
6363
const optimal = optimizationFunction(stats, balancedWeight);
64+
const onlyColdStarts = event.onlyColdStarts;
65+
const num = event.num;
6466

6567
// also compute total cost of optimization state machine & lambda
6668
optimal.stateMachine = {};
67-
optimal.stateMachine.executionCost = utils.stepFunctionsCost(event.stats.length);
69+
optimal.stateMachine.executionCost = utils.stepFunctionsCost(event.stats.length, onlyColdStarts, num);
6870
optimal.stateMachine.lambdaCost = stats
6971
.map((p) => p.totalCost)
7072
.reduce((a, b) => a + b, 0);

lambda/cleaner.js

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,20 @@ const utils = require('./utils');
88
*/
99
module.exports.handler = async(event, context) => {
1010

11-
const {lambdaARN, powerValues} = event;
11+
const {
12+
lambdaARN,
13+
powerValues,
14+
onlyColdStarts,
15+
num,
16+
} = extractDataFromInput(event);
1217

1318
validateInput(lambdaARN, powerValues); // may throw
1419

15-
const ops = powerValues.map(async(value) => {
16-
const alias = 'RAM' + value;
17-
await cleanup(lambdaARN, alias); // may throw
20+
// build list of aliases to clean up
21+
const aliases = buildAliasListForCleanup(lambdaARN, onlyColdStarts, powerValues, num);
22+
23+
const ops = aliases.map(async(alias) => {
24+
await cleanup(lambdaARN, alias);
1825
});
1926

2027
// run everything in parallel and wait until completed
@@ -23,12 +30,32 @@ module.exports.handler = async(event, context) => {
2330
return 'OK';
2431
};
2532

33+
const buildAliasListForCleanup = (lambdaARN, onlyColdStarts, powerValues, num) => {
34+
if (onlyColdStarts){
35+
return powerValues.map((powerValue) => {
36+
return utils.range(num).map((index) => {
37+
return utils.buildAliasString(`RAM${powerValue}`, onlyColdStarts, index);
38+
});
39+
}).flat();
40+
}
41+
return powerValues.map((powerValue) => utils.buildAliasString(`RAM${powerValue}`));
42+
};
43+
44+
const extractDataFromInput = (event) => {
45+
return {
46+
lambdaARN: event.lambdaARN,
47+
powerValues: event.lambdaConfigurations.powerValues,
48+
onlyColdStarts: event.onlyColdStarts,
49+
num: parseInt(event.num, 10), // parse as we do in the initializer
50+
};
51+
};
52+
2653
const validateInput = (lambdaARN, powerValues) => {
2754
if (!lambdaARN) {
2855
throw new Error('Missing or empty lambdaARN');
2956
}
3057
if (!powerValues || !powerValues.length) {
31-
throw new Error('Missing or empty power values');
58+
throw new Error('Missing or empty powerValues values');
3259
}
3360
};
3461

lambda/executor.js

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ module.exports.handler = async(event, context) => {
2020
preProcessorARN,
2121
postProcessorARN,
2222
discardTopBottom,
23+
onlyColdStarts,
2324
sleepBetweenRunsMs,
2425
disablePayloadLogs,
2526
} = await extractDataFromInput(event);
@@ -35,8 +36,11 @@ module.exports.handler = async(event, context) => {
3536
const lambdaAlias = 'RAM' + value;
3637
let results;
3738

38-
// fetch architecture from $LATEST
39-
const {architecture, isPending} = await utils.getLambdaConfig(lambdaARN, lambdaAlias);
39+
// defaulting the index to 0 as the index is required for onlyColdStarts
40+
let aliasToInvoke = utils.buildAliasString(lambdaAlias, onlyColdStarts, 0);
41+
// We need the architecture, regardless of onlyColdStarts or not
42+
const {architecture, isPending} = await utils.getLambdaConfig(lambdaARN, aliasToInvoke);
43+
4044
console.log(`Detected architecture type: ${architecture}, isPending: ${isPending}`);
4145

4246
// pre-generate an array of N payloads
@@ -49,12 +53,14 @@ module.exports.handler = async(event, context) => {
4953
payloads: payloads,
5054
preARN: preProcessorARN,
5155
postARN: postProcessorARN,
56+
onlyColdStarts: onlyColdStarts,
5257
sleepBetweenRunsMs: sleepBetweenRunsMs,
5358
disablePayloadLogs: disablePayloadLogs,
5459
};
5560

5661
// wait if the function/alias state is Pending
57-
if (isPending) {
62+
// in the case of onlyColdStarts, we will verify each alias in the runInParallel or runInSeries
63+
if (isPending && !onlyColdStarts) {
5864
await utils.waitForAliasActive(lambdaARN, lambdaAlias);
5965
console.log('Alias active');
6066
}
@@ -97,8 +103,14 @@ const extractDiscardTopBottomValue = (event) => {
97103
// extract discardTopBottom used to trim values from average duration
98104
let discardTopBottom = event.discardTopBottom;
99105
if (typeof discardTopBottom === 'undefined') {
106+
// default value for discardTopBottom
100107
discardTopBottom = 0.2;
101108
}
109+
// In case of onlyColdStarts, we only have 1 invocation per alias, therefore we shouldn't discard any execution
110+
if (event.onlyColdStarts){
111+
discardTopBottom = 0;
112+
console.log('Setting discardTopBottom to 0, every invocation should be accounted when onlyColdStarts');
113+
}
102114
// discardTopBottom must be between 0 and 0.4
103115
return Math.min(Math.max(discardTopBottom, 0.0), 0.4);
104116
};
@@ -128,16 +140,22 @@ const extractDataFromInput = async(event) => {
128140
preProcessorARN: input.preProcessorARN,
129141
postProcessorARN: input.postProcessorARN,
130142
discardTopBottom: discardTopBottom,
143+
onlyColdStarts: !!input.onlyColdStarts,
131144
sleepBetweenRunsMs: sleepBetweenRunsMs,
132145
disablePayloadLogs: !!input.disablePayloadLogs,
133146
};
134147
};
135148

136-
const runInParallel = async({num, lambdaARN, lambdaAlias, payloads, preARN, postARN, disablePayloadLogs}) => {
149+
const runInParallel = async({num, lambdaARN, lambdaAlias, payloads, preARN, postARN, disablePayloadLogs, onlyColdStarts}) => {
137150
const results = [];
138151
// run all invocations in parallel ...
139152
const invocations = utils.range(num).map(async(_, i) => {
140-
const {invocationResults, actualPayload} = await utils.invokeLambdaWithProcessors(lambdaARN, lambdaAlias, payloads[i], preARN, postARN, disablePayloadLogs);
153+
let aliasToInvoke = utils.buildAliasString(lambdaAlias, onlyColdStarts, i);
154+
if (onlyColdStarts){
155+
await utils.waitForAliasActive(lambdaARN, aliasToInvoke);
156+
console.log(`${aliasToInvoke} is active`);
157+
}
158+
const {invocationResults, actualPayload} = await utils.invokeLambdaWithProcessors(lambdaARN, aliasToInvoke, payloads[i], preARN, postARN, disablePayloadLogs);
141159
// invocation errors return 200 and contain FunctionError and Payload
142160
if (invocationResults.FunctionError) {
143161
let errorMessage = 'Invocation error (running in parallel)';
@@ -150,11 +168,16 @@ const runInParallel = async({num, lambdaARN, lambdaAlias, payloads, preARN, post
150168
return results;
151169
};
152170

153-
const runInSeries = async({num, lambdaARN, lambdaAlias, payloads, preARN, postARN, sleepBetweenRunsMs, disablePayloadLogs}) => {
171+
const runInSeries = async({num, lambdaARN, lambdaAlias, payloads, preARN, postARN, sleepBetweenRunsMs, disablePayloadLogs, onlyColdStarts}) => {
154172
const results = [];
155173
for (let i = 0; i < num; i++) {
174+
let aliasToInvoke = utils.buildAliasString(lambdaAlias, onlyColdStarts, i);
156175
// run invocations in series
157-
const {invocationResults, actualPayload} = await utils.invokeLambdaWithProcessors(lambdaARN, lambdaAlias, payloads[i], preARN, postARN, disablePayloadLogs);
176+
if (onlyColdStarts){
177+
await utils.waitForAliasActive(lambdaARN, aliasToInvoke);
178+
console.log(`${aliasToInvoke} is active`);
179+
}
180+
const {invocationResults, actualPayload} = await utils.invokeLambdaWithProcessors(lambdaARN, aliasToInvoke, payloads[i], preARN, postARN, disablePayloadLogs);
158181
// invocation errors return 200 and contain FunctionError and Payload
159182
if (invocationResults.FunctionError) {
160183
let errorMessage = 'Invocation error (running in series)';
@@ -169,18 +192,19 @@ const runInSeries = async({num, lambdaARN, lambdaAlias, payloads, preARN, postAR
169192
};
170193

171194
const computeStatistics = (baseCost, results, value, discardTopBottom) => {
172-
// use results (which include logs) to compute average duration ...
173-
174-
const durations = utils.parseLogAndExtractDurations(results);
175195

176-
const averageDuration = utils.computeAverageDuration(durations, discardTopBottom);
196+
// use results (which include logs) to compute average duration ...
197+
const totalDurations = utils.parseLogAndExtractDurations(results);
198+
const averageDuration = utils.computeAverageDuration(totalDurations, discardTopBottom);
177199
console.log('Average duration: ', averageDuration);
178200

179-
// ... and overall statistics
180-
const averagePrice = utils.computePrice(baseCost, minRAM, value, averageDuration);
181-
201+
// ... and overall cost statistics
202+
const billedDurations = utils.parseLogAndExtractBilledDurations(results);
203+
const averageBilledDuration = utils.computeAverageDuration(billedDurations, discardTopBottom);
204+
console.log('Average Billed duration: ', averageBilledDuration);
205+
const averagePrice = utils.computePrice(baseCost, minRAM, value, averageBilledDuration);
182206
// .. and total cost (exact $)
183-
const totalCost = utils.computeTotalCost(baseCost, minRAM, value, durations);
207+
const totalCost = utils.computeTotalCost(baseCost, minRAM, value, billedDurations);
184208

185209
const stats = {
186210
averagePrice,

lambda/initializer.js

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,58 @@ const defaultPowerValues = process.env.defaultPowerValues.split(',');
88
*/
99
module.exports.handler = async(event, context) => {
1010

11-
const {lambdaARN, num} = event;
12-
const powerValues = extractPowerValues(event);
11+
const {
12+
lambdaARN,
13+
num,
14+
powerValues,
15+
onlyColdStarts,
16+
} = extractDataFromInput(event);
1317

1418
validateInput(lambdaARN, num); // may throw
1519

1620
// fetch initial $LATEST value so we can reset it later
17-
const initialPower = await utils.getLambdaPower(lambdaARN);
21+
const {power, description} = await utils.getLambdaPower(lambdaARN);
22+
console.log(power, description);
23+
24+
let initConfigurations = [];
1825

1926
// reminder: configuration updates must run sequentially
2027
// (otherwise you get a ResourceConflictException)
21-
for (let value of powerValues){
22-
const alias = 'RAM' + value;
23-
await utils.createPowerConfiguration(lambdaARN, value, alias);
28+
for (let powerValue of powerValues){
29+
const baseAlias = 'RAM' + powerValue;
30+
if (!onlyColdStarts){
31+
initConfigurations.push({powerValue: powerValue, alias: baseAlias});
32+
} else {
33+
for (let n of utils.range(num)){
34+
let alias = utils.buildAliasString(baseAlias, onlyColdStarts, n);
35+
// here we inject a custom description to force the creation of a new version
36+
// even if the power is the same, which will force a cold start
37+
initConfigurations.push({powerValue: powerValue, alias: alias, description: `${description} - ${alias}`});
38+
}
39+
}
2440
}
41+
// Publish another version to revert the Lambda Function to its original configuration
42+
initConfigurations.push({powerValue: power, description: description});
43+
44+
return {
45+
initConfigurations: initConfigurations,
46+
iterator: {
47+
index: 0,
48+
count: initConfigurations.length,
49+
continue: true,
50+
},
51+
powerValues: powerValues,
52+
};
53+
};
2554

26-
await utils.setLambdaPower(lambdaARN, initialPower);
2755

28-
return powerValues;
56+
const extractDataFromInput = (event) => {
57+
return {
58+
lambdaARN: event.lambdaARN,
59+
num: parseInt(event.num, 10),
60+
powerValues: extractPowerValues(event),
61+
onlyColdStarts: !!event.onlyColdStarts,
62+
};
2963
};
3064

3165
const extractPowerValues = (event) => {

lambda/publisher.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use strict';
2+
3+
const utils = require('./utils');
4+
5+
6+
module.exports.handler = async(event, context) => {
7+
const {lambdaConfigurations, currConfig, lambdaARN} = validateInputs(event);
8+
const currentIterator = lambdaConfigurations.iterator;
9+
// publish version & assign alias (if present)
10+
await utils.createPowerConfiguration(lambdaARN, currConfig.powerValue, currConfig.alias, currConfig.description);
11+
12+
const result = {
13+
powerValues: lambdaConfigurations.powerValues,
14+
initConfigurations: lambdaConfigurations.initConfigurations,
15+
iterator: {
16+
index: (currentIterator.index + 1),
17+
count: currentIterator.count,
18+
continue: ((currentIterator.index + 1) < currentIterator.count),
19+
},
20+
};
21+
22+
if (!result.iterator.continue) {
23+
// clean the list of configuration if we're done iterating
24+
delete result.initConfigurations;
25+
}
26+
27+
return result;
28+
};
29+
function validateInputs(event) {
30+
if (!event.lambdaARN) {
31+
throw new Error('Missing or empty lambdaARN');
32+
}
33+
const lambdaARN = event.lambdaARN;
34+
if (!(event.lambdaConfigurations && event.lambdaConfigurations.iterator && event.lambdaConfigurations.initConfigurations)){
35+
throw new Error('Invalid iterator for initialization');
36+
}
37+
const iterator = event.lambdaConfigurations.iterator;
38+
if (!(iterator.index >= 0 && iterator.index < iterator.count)){
39+
throw new Error(`Invalid iterator index: ${iterator.index}`);
40+
}
41+
const lambdaConfigurations = event.lambdaConfigurations;
42+
const currIdx = iterator.index;
43+
const currConfig = lambdaConfigurations.initConfigurations[currIdx];
44+
if (!(currConfig && currConfig.powerValue)){
45+
throw new Error(`Invalid init configuration: ${JSON.stringify(currConfig)}`);
46+
}
47+
return {lambdaConfigurations, currConfig, lambdaARN};
48+
}

0 commit comments

Comments
 (0)