Skip to content
This repository has been archived by the owner on Jan 28, 2025. It is now read-only.

RFC S3 client revert #1021

Closed

Conversation

jvarho
Copy link
Collaborator

@jvarho jvarho commented Apr 25, 2021

Looking at the generated lambda, the dynamic S3 client imports do not seem to do anything. The code for them gets bundled up and there's just a wrapper promise in place of the await import().

This is a not-for-merging partial revert of f9eef45 that changes it back to the previous approach. I didn't even fix the tests yet.

There's probably a better way to solve this – I've never used rollup so I didn't try any packaging changes – but this more than halved the default-handler size:

Before:

636K	default-handler.js
232K	default-handler.min.js

After:

288K	default-handler.js
104K	default-handler.min.js

It seems to have shaved some memory and initialization time off the lambda as well, although that was just based on a few poorly controlled test runs. But I've not measured what happens to fallback response times.

@jvarho jvarho changed the title Example s3 client revert RFC S3 client revert Apr 25, 2021
@dphang
Copy link
Collaborator

dphang commented Apr 25, 2021

The reason for the drastic reduction in handler size is because the revert would now use back the default aws-sdk that's built into the lambda, so no code needs to be bundled. However, one problem is that when you import even just the S3 part, it imports all S3 operations, which I found caused poor cold start times (added 100 ms or so)...I did test this, will try to find the past issue and link it here.

However, now it's using the aws sdk v3, which is not in Lambda yet, so its code has to be bundled. But all operations are modularized so we end up only needing to import S3 operations we need (get and put). I believe it only added a few ms (around 3 ms) to the cold start times.

Let me double check on the dynamic imports thing if it's working correctly, I believe it was. But either way even if it's not, the cold start should be much better.

@jvarho
Copy link
Collaborator Author

jvarho commented Apr 25, 2021

However, now it's using the aws sdk v3, which is not in Lambda yet, so its code has to be bundled. But all operations are modularized so we end up only needing to import S3 operations we need (get and put). I believe it only added a few ms (around 3 ms) to the cold start times.

Yes, I get that it is more efficient to be able to import just the parts used. But moving the old aws sdk import to be as-needed, like I did in this pseudo-revert, avoids the cold start penalty until the fallback paths are hit (which might be never). My admittedly poor testing seemed to indicate a performance boost.

But I agree this is likely not the correct change, like I wrote above.

@dphang
Copy link
Collaborator

dphang commented Apr 25, 2021

Ok, let me test it out again and confirm.

1 similar comment
@dphang
Copy link
Collaborator

dphang commented Apr 25, 2021

Ok, let me test it out again and confirm.

@dphang
Copy link
Collaborator

dphang commented Apr 26, 2021

So this was the original issue: #580.

I believe I saw ~250 ms cold start init time but that was AWS-SDK (built-in to Lambda, i.e v2) being initialized on cold start outside the handler (so every time even if it's not used) and when using the S3 client there, AFAIK, negligible init time (bulk was already initialized in cold start). Then with the AWS-SDK v3, I used the modularized client but it adds extra code to the handler, but is dynamically imported. I saw during cold starts, init time drop to ~180 ms and then the dynamic imports were ~3 ms, for a total of ~183 ms. I thought cold start due to S3 client was just ~3 ms, but yeah, it seems there could be cold start outside of the handler that a user would always pay for even if they don't use s3 client.

I guess yours is slightly different since you moved AWS-SDK v2 to be a dynamic import instead. So I need to measure what the new cold start times are: 1) outside handler time (should be none added here besides other core initialization due to dynamic import) + 2) dynamic import time. Will try to setup a test later this week.

Test results (will update as I added more). Using Node.js 12.x and 512 MB memory.

fallback page s3 get (cold start) - init duration is 193.22 ms and dynamic import (albeit maybe it's not doing much since most of the code was already declared outside) is < 1 ms

2021-04-25T19:08:18.909-07:00 | START RequestId: 5821ba98-9710-466f-be6b-d88b7d7c096b Version: 142
-- | --
  | 2021-04-25T19:08:18.913-07:00 | END RequestId: 5821ba98-9710-466f-be6b-d88b7d7c096b
  | 2021-04-25T19:08:18.913-07:00 | REPORT RequestId: 5821ba98-9710-466f-be6b-d88b7d7c096b Duration: 3.93 ms Billed Duration: 4 ms Memory Size: 512 MB Max Memory Used: 71 MB Init Duration: 193.22 ms
  | 2021-04-25T19:08:19.277-07:00 | START RequestId: 10d9a2e8-78e2-4d49-a902-c9da3af13d2e Version: 142
  | 2021-04-25T19:08:19.282-07:00 | 2021-04-26T02:08:19.281Z 10d9a2e8-78e2-4d49-a902-c9da3af13d2e INFO Time before s3 client: 536.7126539999153
  | 2021-04-25T19:08:19.282-07:00 | 2021-04-26T02:08:19.282Z 10d9a2e8-78e2-4d49-a902-c9da3af13d2e INFO Time after s3 client: 537.4100739997812
  | 2021-04-25T19:08:19.284-07:00 | 2021-04-26T02:08:19.284Z 10d9a2e8-78e2-4d49-a902-c9da3af13d2e INFO Time before s3 get: 539.0748269998003
  | 2021-04-25T19:08:19.284-07:00 | 2021-04-26T02:08:19.284Z 10d9a2e8-78e2-4d49-a902-c9da3af13d2e INFO Time after s3 get: 539.1743659998756

Regular SSG page get, which won't import any S3 code (cold start) - init duration is 198.57 ms

2021-04-25T19:14:19.801-07:00 | START RequestId: 66013b81-fc55-4e7e-b991-34169392d4a8 Version: 144
-- | --
  | 2021-04-25T19:14:19.805-07:00 | END RequestId: 66013b81-fc55-4e7e-b991-34169392d4a8
  | 2021-04-25T19:14:19.805-07:00 | REPORT RequestId: 66013b81-fc55-4e7e-b991-34169392d4a8 Duration: 3.60 ms Billed Duration: 4 ms Memory Size: 512 MB Max Memory Used: 69 MB Init Duration: 198.57 ms

Now I'll test with your changes (using built-in AWS-SDK v2, dynamically importing S3 client only when needed).

Fallback page s3 get (cold start): init duration is 170.62 ms and then in origin response handler it is 331 ms to initialize aws-sdk s3 client (v2) dynamically.

2021-04-25T19:24:57.233-07:00 | START RequestId: 963b2969-f76a-4a22-bc58-fe054c0b1013 Version: 146
-- | --
  | 2021-04-25T19:24:57.237-07:00 | END RequestId: 963b2969-f76a-4a22-bc58-fe054c0b1013
  | 2021-04-25T19:24:57.237-07:00 | REPORT RequestId: 963b2969-f76a-4a22-bc58-fe054c0b1013 Duration: 4.10 ms Billed Duration: 5 ms Memory Size: 512 MB Max Memory Used: 67 MB Init Duration: 170.62 ms
  | 2021-04-25T19:24:57.562-07:00 | START RequestId: a15ddf04-b0d2-4276-803d-a93c2eb2475f Version: 146
  | 2021-04-25T19:24:57.566-07:00 | 2021-04-26T02:24:57.566Z a15ddf04-b0d2-4276-803d-a93c2eb2475f INFO Time before s3 client: 471.77653999999166
  | 2021-04-25T19:24:57.897-07:00 | 2021-04-26T02:24:57.896Z a15ddf04-b0d2-4276-803d-a93c2eb2475f INFO Time after s3 client: 802.6060140000191

Regular SSG page: as expected just 179.05 ms since there is no AWS-SDK v2 s3 initialization.

2021-04-25T19:30:18.282-07:00 | START RequestId: dae14970-5d14-4611-a595-594cf7c6c87d Version: 148
-- | --
  | 2021-04-25T19:30:18.286-07:00 | END RequestId: dae14970-5d14-4611-a595-594cf7c6c87d
  | 2021-04-25T19:30:18.286-07:00 | REPORT RequestId: dae14970-5d14-4611-a595-594cf7c6c87d Duration: 4.29 ms Billed Duration: 5 ms Memory Size: 512 MB Max Memory Used: 67 MB Init Duration: 179.05 ms

@dphang
Copy link
Collaborator

dphang commented Apr 26, 2021

In conclusion, yes it does look like the use of AWS-SDK v3 for its modularized s3 client is adding a bit of cold start time to all requests, maybe around 10-15 ms? Though I think it is much better than AWS-SDK v2 which is taking ~330 ms to dynamically import the client (since it gets all S3 operations, not just GET/PUT), albeit this only occurs when fallback path is hit.

Ideally, AWS Lambda should hopefully make AWS-SDK v3 available in the Lambda environment soon. Meanwhile, I think we can investigate how to get Rollup.js to inline all the S3 GET/PUT code into that line where we dynamically import S3 client.

@jvarho
Copy link
Collaborator Author

jvarho commented Apr 26, 2021

In conclusion, yes it does look like the use of AWS-SDK v3 for its modularized s3 client is adding a bit of cold start time to all requests, maybe around 10-15 ms? Though I think it is much better than AWS-SDK v2 which is taking ~330 ms to dynamically import the client (since it gets all S3 operations, not just GET/PUT), albeit this only occurs when fallback path is hit.

Thanks for confirming!

Meanwhile, I think we can investigate how to get Rollup.js to inline all the S3 GET/PUT code into that line where we dynamically import S3 client.

Yeah, or the S3 client could be bundled in a separate file that is dynamically imported.

@dphang
Copy link
Collaborator

dphang commented Apr 26, 2021

In conclusion, yes it does look like the use of AWS-SDK v3 for its modularized s3 client is adding a bit of cold start time to all requests, maybe around 10-15 ms? Though I think it is much better than AWS-SDK v2 which is taking ~330 ms to dynamically import the client (since it gets all S3 operations, not just GET/PUT), albeit this only occurs when fallback path is hit.

Thanks for confirming!

Meanwhile, I think we can investigate how to get Rollup.js to inline all the S3 GET/PUT code into that line where we dynamically import S3 client.

Yeah, or the S3 client could be bundled in a separate file that is dynamically imported.

So I tried some things and we could solve it using a Rollup.js config like so:

const generateConfig = (input) => ({
  input: `./src/${input.filename}.ts`,
  output: {
    dir: "./dist/",
    entryFileNames: `${input.filename}${input.minify ? ".min" : ""}.js`,
    format: "cjs",
    manualChunks(id) {
      if (id.includes("aws-sdk")) {
        return "aws-sdk";
      } else {
        return "serverless-next-js";
      }
    }
  },
  plugins: [
    json(),
    commonjs(),
    externals({
      exclude: "@sls-next/next-aws-cloudfront"
    }),
    nodeResolve(),
    typescript({
      tsconfig: "tsconfig.bundle.json"
    }),
    input.minify
      ? terser({
          compress: true,
          mangle: true,
          output: { comments: false } // Remove all comments, which is fine as the handler code is not distributed.
        })
      : undefined
  ],
  external: [...NPM_EXTERNALS, ...LOCAL_EXTERNALS],
  inlineDynamicImports: false
});

The magic is basically in manualChunks which is allowing us to create a chunk specifically for aws-sdk and rest is in serverless-next-js chunk. However, the two caveats are:

  1. Every chunk is minified regardless of minify option (the minify option in the config seems to only apply to the entry file, not dependent chunks)
  2. The entry file is just a small wrapper of ~10 lines, real code will be in generated serverless-next-js-<hash>.js file and the entry file will import this. It will be something like this Entry -> Serverless-next.js core code -> AWS-SDK S3 code. However, we want something like this Entry (with core code) -> AWS-SDK S3 code, so we don't need an extra file lookup for no reason.

It might just be a matter of playing around with Rollup.js config to get to where we want.

EDIT: for (2) it looks like we can just return null for any module besides aws-sdk, it will just create the default-handler and aws-sdk chunks. However, aws-sdk is still minified (might be fine though since it's external). Also it seems that aws-sdk is still required outside the handler and calling things like awsSdk.createCommonjsModule - need to figure out how to avoid this

@jvarho jvarho force-pushed the dynamic-aws-sdk-import branch from 0f56398 to 2b880a3 Compare April 28, 2021 14:50
@jvarho
Copy link
Collaborator Author

jvarho commented Apr 28, 2021

@dphang Again, no experience with rollup... This produces more or less the desired result, I think. But maybe too ugly and may not resolve types correctly?

@codecov
Copy link

codecov bot commented Apr 28, 2021

Codecov Report

Merging #1021 (910dae4) into master (f18d6a6) will increase coverage by 0.00%.
The diff coverage is 100.00%.

❗ Current head 910dae4 differs from pull request most recent head eefb670. Consider uploading reports for the commit eefb670 to get more accurate results
Impacted file tree graph

@@           Coverage Diff           @@
##           master    #1021   +/-   ##
=======================================
  Coverage   82.32%   82.33%           
=======================================
  Files          69       70    +1     
  Lines        2574     2575    +1     
  Branches      613      613           
=======================================
+ Hits         2119     2120    +1     
  Misses        389      389           
  Partials       66       66           
Impacted Files Coverage Δ
packages/libs/lambda-at-edge/src/build.ts 95.33% <ø> (ø)
...ackages/libs/lambda-at-edge/src/default-handler.ts 93.39% <100.00%> (ø)
packages/libs/lambda-at-edge/src/s3-client.ts 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update f18d6a6...eefb670. Read the comment docs.

@dphang
Copy link
Collaborator

dphang commented Apr 28, 2021

@dphang Again, no experience with rollup... This produces more or less the desired result, I think. But maybe too ugly and may not resolve types correctly?

Yeah no worries, I was trying to check if we can do it in rollup but it seems it's still doing some stuff outside the handler... seems it might not handle dynamic imports well? maybe I'll create an issue in Rollup to ask how to do it correctly as I couldn't figure it out.

I will check your solution and see how it performs, I think it seems ok.

EDIT: testing out proper bundling config in #1029.

@dphang dphang mentioned this pull request Apr 28, 2021
13 tasks
@jvarho
Copy link
Collaborator Author

jvarho commented Apr 29, 2021

Obsolete

@jvarho jvarho closed this Apr 29, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants