Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
January 24, 2022 11:50 am GMT

How to Use Source Maps in TypeScript Lambda Functions (with Benchmarks)

TypeScript is a popular language for developers of all kinds and it's made its mark on the serverless space. Most of the major Lambda frameworks now have solid TypeScript support. The days of struggling with webpack configurations are mostly behind us.

Table of Contents

Stack Traces

If we're already transpiling our code to convert the TypeScript source to Lambda-friendly JavaScript, we might as well go ahead and minify and tree-shake the code as well. Smaller bundles can make deployments faster and may even help with cold start and execution time. However, this can make debugging difficult when we start seeing stack traces that look like this:

{    "errorType": "SyntaxError",    "errorMessage": "Unexpected end of JSON input",    "stack": [        "SyntaxError: Unexpected end of JSON input",        "    at JSON.parse (<anonymous>)",        "    at VA (/var/task/index.js:25:69708)",        "    at Runtime.R8 [as handler] (/var/task/index.js:25:69808)",        "    at Runtime.handleOnce (/var/runtime/Runtime.js:66:25)"    ]}

The error message here lets us know that we're failing to parse a JSON string, but the stack trace itself is useless for finding the line where the code is failing. We'll have no choice but to look through our code and hope to find the error. We might be able to do a text search for JSON.parse but if that is happening in one of our dependencies, searching won't work. What's next? Add a bunch of log statements to the code? No! If we use Source Maps, we can get more useful stack traces:

{    "errorType": "SyntaxError",    "errorMessage": "Unexpected end of JSON input",    "stack": [        "SyntaxError: Unexpected end of JSON input",        "    at JSON.parse (<anonymous>)",        "    at VA (/fns/db.ts:39:8)",        "    at Runtime.R8 (/fns/list.ts:6:24)",        "    at Runtime.handleOnce (/var/runtime/Runtime.js:66:25)"    ]}

Now we can see the stack includes a call from line 6 of list.ts which finally fails on line 39 of db.ts.

To enable Source Maps in our application, we need to tell our build tool to emit Source Maps and we need to enable Source Map support in our runtime.

Emitting Source Maps

Emitting Source Maps is very easy with esbuild. We simply set the boolean property in our configuration. Now when we run the build, we'll get an index.js.map file as well as our index.js. This file must be uploaded to the Lambda service. We'll see how to do that with AWS CDK, AWS SAM and the Serverless Framework a bit later in this article.

Source Map Support

Having the index.js.map file in our Lambda runtime isn't sufficient to enable Source Maps. We also have to make sure the runtime knows to make use of them. Fortunately this is very easy ever since Node.js version 12.12.0. We just have to set the --enable-source-maps command line option. Command line options can be set in AWS Lambda by setting the NODE_OPTIONS environment variable. At the time of this writing, AWS Lambda supports Node.js versions 12 and 14. AWS does not publish the minor versions in use in Lambda, but we can discover it by logging out process.version in a function. As of late January, 2022, the Node.js versions in use in Lambda in us-east-1 are v12.22.7 and v14.18.1 so we'll have no trouble using Source Maps.

If we needed to enable Source Maps in a runtime that doesn't support the native version, we could always use Source Map Support.

CDK Example

All example code is available on GitHub.

AWS CDK is my preferred tool for writing and deploying serverless applications, in part because of the aws-lambda-nodejs construct. This construct makes it very easy to work with TypeScript. It wraps esbuild and exposes options. It also supports setting environment variables.

When I'm working with multiple Lambda functions, I often find it helpful to create a single props object that then gets shared among multiple functions.

const lambdaProps = {  architecture: Architecture.ARM_64,  bundling: { minify: true, sourceMap: true },  environment: {    NODE_OPTIONS: '--enable-source-maps',  },  logRetention: RetentionDays.ONE_DAY,  runtime: Runtime.NODEJS_14_X,  memorySize: 512,  timeout: Duration.minutes(1),};new NodejsFunction(this, 'FuncOne', {  ...lambdaProps,  entry: `${__dirname}/../fns/one.ts`,});new NodejsFunction(this, 'FuncTwo', {  ...lambdaProps,  entry: `${__dirname}/../fns/two.ts`,});

As we can see, it's very simple to enable Source Maps when already using AWS CDK and NodejsFunction.

Serverless Stack

Serverless Stack is a very cool value add that builds on top of AWS CDK delivering an awesome developer experience and dashboard. Unfortunately SST's own version of NodejsFunction, Function, doesn't support Source Maps or allow the passing of custom config options to esbuild. Hopefully this can be supported soon.

SAM Example

SAM support for TypeScript has been lagging for some time, but a pull request was just merged that should change that. It looks like we'll be able to add an aws_sam key in the package.json file to enable building within the SAM engine as part of sam build. Although this PR has been merged to aws-lambda-builders, the engine behind sam build, it will still need to be added to aws-sam-cli and released (to much fanfare, one expects) before it can be used with SAM.

Meanwhile - or if we're considering other options for deploying our functions - we can add an extra build step. We'll create an esbuild.ts file that transpiles the functions and then point our SAM template.yaml file at the output of that step.

import { build, BuildOptions } from 'esbuild';const buildOptions: BuildOptions = {  bundle: true,  entryPoints: {    ['create/index']: `${__dirname}/../fns/create.ts`,    ['delete/index']: `${__dirname}/../fns/delete.ts`,    ['list/index']: `${__dirname}/../fns/list.ts`,  },  minify: true,  outbase: 'fns',  outdir: 'sam/build',  platform: 'node',  sourcemap: true,};build(buildOptions);

SAM templates will take everything in the CodeUri so the above code will transpile a TypeScript file at fns/list.ts and output sam/build/list/index.js and sam/build/list/index.js.map. By setting CodeUri: sam/build/list, SAM will package the two files and upload them to Lambda.

Now we just need to set the environment variable. This is easy enough to do in a SAM template. We can set it as a global so it only needs to be in the template once

Globals:  Function:    Environment:      Variables:        NODE_OPTIONS: '--enable-source-maps'

In order to make sure we always build before deploying, we can add some npm scripts.

"scripts": {  "build:lambda": "npm run clean && ts-node --files sam/esbuild.ts",  "clean": "rimraf cdk.out sam/build",  "deploy:sam": "npm run build:lambda && sam deploy --template template.yaml",  "destroy:sam": "sam delete"}

This works well enough, but does take some extra effort. SAM users are no doubt eagerly awaiting better TypeScript support.

Architect

Alternately, use Architect. Architect is a 3rd party developer experience that builds on top of AWS SAM. Architect includes a TypeScript plugin.

Serverless Framework Example

The Serverless Framework is known for its plugin system. serverless-esbuild brings bundling and all the options we need to support Source Maps in TypeScript into the sls package and sls deploy commands.

The plugin is configured in our serverless.yml file.

custom:  esbuild:    bundle: true    minify: true    sourcemap: true

And then we just point our functions at our TypeScript handlers.

functions:  create:    handler: fns/create.handler

Much like SAM, Serverless lets us set global environment variables for our functions.

provider:  name: aws  lambdaHashingVersion: 20201221  runtime: nodejs14.x  environment:    NODE_OPTIONS: '--enable-source-maps'

Much like AWS CDK, this is a good experience for TypeScript developers. Those already using Serverless Framework should have an easy time adding Source Maps to their applications.

Benchmarks

There's a lot of guidance against using Source Maps in production because of a supposed negative performance impact. Let's measure the admittedly-simple list function and see if minification or the use of source maps has any noticeable effect.

I used autocannon to test the function at 100 concurrent executions for 30 seconds. I also used Lambda Power Tuning to find the ideal memory configuration, which proved to be 512MB. All the results are available.

Not Minified without Source Maps

The unminified function is 1.2MB in size. This is mostly from @aws-sdk/client-dynamodb and @aws-sdk/lib-dynamodb. Whether sticking with SDK v2 might be better performance would be a good topic for another post. This function is over one MB, despite a small amount of custom code.

Running the test, the function has an average execution of 46.99ms and a max of 914ms. 99% of my requests are at or below 90ms.

Minified without Source Maps

Minifying the function drops the size to 534.8kb, less than half the size. My test showed an average response of 47.83ms, a max of 1010ms and 90ms at the 99th percentile. This is slightly worse, but not statistically significant. I expect if I ran these tests over and over again, I would see that minifying the code has no real effect on performance. This is not a surprise. 1.2MB is still fairly small and I don't expect to see much in the way of added latency at that size.

Minified with Source Maps

The minified function is still 534.8kb, of course, but the Source Map is 1.5MB so this will be the largest upload, not that ~2MB is a lot or will significantly slow down our deployments.

The average response time for this test is 46.52ms, max is 968mb and the 99th percentile is 82ms. This is the best result so far, but not statistically significant. I would say this is truly a three-way tie which tells us that adding Source Maps to this function did not increase latency.

That's because of the native support in Node.js! Since no stack traces were emitted, the Source Maps were never referenced. It's possible if we had to rely on a library for this capability, we wouldn't have the same outcome.

Error Minified without Source Maps

Will triggering error conditions make a difference? Let's see. I ran the same test but this time the function has that JSON.parse error in it. This happens before the call to DynamoDB, so we can expect it to be a little faster than the successful function. We get 37.86ms as the average, 1004ms as the max and 58ms at 99%. It's impressive the call to DynamoDB only seems to add about 10ms of latency!

Error Minified with Source Maps

Enabling Source Maps for the errors does impact performance. Our average has dropped to 97.54ms, max at 1129ms and 99% is 243. This is a significant increase. Source Maps do impact latency when an error occurs. This makes sense and confirms the idea that the Source Maps are only referenced when an error occurs - but now that Source Map must be parsed and that takes time.

Conclusion

Use Source Maps in production! In my view, if you are getting so many errors that the performance hit from Source Maps is impacting your bottom line, you should probably go and fix those errors. It's very easy to implement Source Maps in a variety of popular Lambda frameworks and they don't impact successful execution. When functions do fail, the useful stack trace is going to be worth the added latency. Developer hours are always going to be more expensive than a few milliseconds of Lambda execution.

COVER


Original Link: https://dev.to/aws-builders/how-to-use-source-maps-in-typescript-lambda-functions-with-benchmarks-4bo4

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To