Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: advanced benchmarking #25

Draft
wants to merge 29 commits into
base: main
Choose a base branch
from

Conversation

jpbourgeon
Copy link

@jpbourgeon jpbourgeon commented Apr 23, 2023

Hi

  • I refactored the project architecture. Now the benchmark workspace is @itty-aws/benchmark
  • I changed the tsconfig setup to exclude the benchmark package from the build process
  • The fix to package.json proposed in chore: add "main" to package.json to fix compatibility with some frameworks #24 is also included
  • The benchmark package is documented in /benchmark/README.md
  • There is a first benchmark report in /benchmark/data/advanced-benchmarking/README.md

To keep things seperated, I propose that we talk about the result of the benchmark in issue #21 (project status)

Closes #22, Closes #25

@jpbourgeon
Copy link
Author

The scripts are a bit "rough around the edges" and would definitely benefit from a good refactoring. However, I wanted to publish the results as soon as possible.

Copy link
Owner

@sam-goodwin sam-goodwin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks so much for putting this together! It's super thorough and very helpful.

I added some comments but nothing too critical. Just questions and some proposals for simplifying the impl.

Comment on lines 68 to 70
const gitBranch = execSync("git rev-parse --abbrev-ref HEAD 2>/dev/null", {
encoding: "utf-8",
}).trim();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: use async IO

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See 22886aa

const raw: IResult = JSON.parse(
await fsPromises.readFile(rawFilePath, "utf8")
);
let result: IResult = {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can it be const?

let count = { functionCalls: 0, logEntries: 0 };
let consolidatedEntry: Record<string, any> = {};
let requestId: string | undefined;
for (const entry of raw.log) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some comments might be helpful - it looks like this is aggregating the results of multiple runs into a single result?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See 22886aa

console.log(`- Build datasets`);
const datasetPointers: string[] = [];
for (const entry of result.log) {
const label = entry.name.substring(0, entry.name.lastIndexOf("-"));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm curious, do we control the format of this log? Is there a reason it's not in an easy to parse JSON format?

Copy link
Author

@jpbourgeon jpbourgeon Apr 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stack deploys n instances of the same lambda (10 by default). the instance name is suffixed with its iteration number. Thus, we need to strip the suffix to consolidate the data of the same kind of a function.
And of course CloudWatch log is text only. You need to find your json in the message, extract the logs of the API call and then you can parse it.
Only then comes the aggregation part by type and metric.

Comment on lines 199 to 219
if (!result.datasets.coldStarts.totalDuration[label]) {
datasetPointers.push(
`datasets.${targetDataset}.totalDuration.${label}`
);
result.datasets.coldStarts.totalDuration["title"] =
"Cold start: total duration";
result.datasets.coldStarts.totalDuration[label] = {
label: label,
order: CONFIG.functions.reduce(
(prev, curr) =>
curr.functionName === label ? curr.chart.order : prev,
0
),
data: [entry.function.initDuration + entry.function.duration],
};
} else {
result.datasets.coldStarts.totalDuration[label].data.push(
entry.function.initDuration + entry.function.duration
);
}
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, I'd recommend extracting these into dedicated functions and leaning towards pure functions instead of mutating state. I'm not too pure/opinionated, but procedures with a ton of mutation operations can be hard to refactor and read. This could be computeTotalDuration and return the properties you need. Extract each computation into pure functions and then compose them together with map/reduce logic.

Feel free to ignore this feedback as it would require substantial rework which is not necessary at this time.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. This is part of the refactoring I mentioned previously. I posted the results quickly however I will do some refactoring now to avoid technical debt accumulation.

Comment on lines 76 to 77
const command = new DeleteLogGroupCommand({ logGroupName });
console.log(` - log group '${logGroupName}'`);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are you deleting cloudwatch logs?

Copy link
Author

@jpbourgeon jpbourgeon Apr 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I flush any logs from potential previous runs to avoid data corruption.
It will also be useful to prevent redeploying the benchmark stack across features/branche if it has not changed

Comment on lines 79 to 81
const cloudwatchClient = new CloudWatchLogsClient({});
await cloudwatchClient.send(command);
cloudwatchClient.destroy();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why create and destroy? Usually better to create once per node runtime

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is mandatory. I issued random timeouts from the SDK when reusing the same instance. I had no other choice but creating a fresh instance every time ... 😞

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm that is odd. Not sure why a separate instance would fix that. I'd be interested in seeing if that's still the behavior

Comment on lines 96 to 98
} catch {
null;
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: why is null here

@@ -1,9 +1,11 @@
{
"extends": "./tsconfig.base.json",
"include": ["src"],
"include": ["src/**/*"],
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think this is necssary is it?

Comment on lines +1 to +7
{
"files": [],
"references": [
{ "path": "./tsconfig.esm.json" },
{ "path": "./tsconfig.test.json" }
]
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be the default tsconfig.json?

Copy link
Author

@jpbourgeon jpbourgeon Apr 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so.
I didn't want the build command to build the benchmark. This is handled by the CDK. That's why I removed the benchmark project from tsconfig.build.json.
However I left every project in the default tsconfig.json for the IDE. I'm not sure about the consequences of not referencing every project in the root tsconfig.json, however I think it may cause unwanted side effects like unnecessary TSServer reloads when switching files.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I usually have the root tsconfig.json reference every sub project because the IDE uses the root by default. The consequence is that it will build everything when you run tsc-b from the root, which is probably what we want. Not high priority - we can figure it out.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We agree. That's what is implemented here: tsconfig.json references every sub project including @itty-aws/benchmark for the IDE.

But this is tsconfig.build.json that we are talking about, and I removed benchmark/tsconfig.json from it since the benchmark sub-project is not to be build through the main pnpm build command. In this subproject, the CDK takes care of building the lambda functions automatically with esbuild when deploying through pnpm bench:deploy.

@jpbourgeon
Copy link
Author

Thanks so much for putting this together! It's super thorough and very helpful.

I added some comments but nothing too critical. Just questions and some proposals for simplifying the impl.

I will do a thorough refactoring of the benchmark. Scripts do too many things at once and I'm going to break them down into simpler functions. I will also reshape the data model and the corresponding types to simplify the implementation.

I will include your reviews and comments in the refactoring, as they all make sense. Thank you for your much appreciated feedback.

@jpbourgeon jpbourgeon marked this pull request as draft May 5, 2023 10:14
@sam-goodwin
Copy link
Owner

I saw that you recently pushed some changes. Ready for another review? I also need to take some time to fix the build - something is up with that.

@jpbourgeon
Copy link
Author

I'm in the final stretch of my refactoring. I'll let you know as soon as I'm done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Compare library performance against AWS-SDK
2 participants