diff --git a/.github/workflows/aws-lambda-java-profiler.yml b/.github/workflows/aws-lambda-java-profiler.yml
new file mode 100644
index 00000000..7eeb75b2
--- /dev/null
+++ b/.github/workflows/aws-lambda-java-profiler.yml
@@ -0,0 +1,74 @@
+name: Run integration tests for aws-lambda-java-profiler
+
+on:
+ pull_request:
+ branches: [ '*' ]
+ paths:
+ - 'aws-lambda-java-profiler/**'
+ - '.github/workflows/aws-lambda-java-profiler.yml'
+ push:
+ branches: ['*']
+ paths:
+ - 'aws-lambda-java-profiler/**'
+ - '.github/workflows/aws-lambda-java-profiler.yml'
+
+jobs:
+
+ build:
+ runs-on: ubuntu-latest
+
+ permissions:
+ id-token: write
+ contents: read
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK
+ uses: actions/setup-java@v4
+ with:
+ java-version: 21
+ distribution: corretto
+
+ - name: Issue AWS credentials
+ uses: aws-actions/configure-aws-credentials@v4
+ with:
+ aws-region: ${{ secrets.AWS_REGION_PROFILER_EXTENSION_INTEGRATION_TEST }}
+ role-to-assume: ${{ secrets.AWS_ROLE_PROFILER_EXTENSION_INTEGRATION_TEST }}
+ role-session-name: GitHubActionsRunIntegrationTests
+ role-duration-seconds: 900
+
+ - name: Build layer
+ working-directory: ./experimental/aws-lambda-java-profiler/extension
+ run: ./build_layer.sh
+
+ - name: Publish layer
+ working-directory: ./experimental/aws-lambda-java-profiler
+ run: ./integration_tests/publish_layer.sh
+
+ - name: Create the bucket layer
+ working-directory: ./experimental/aws-lambda-java-profiler
+ run: ./integration_tests/create_bucket.sh
+
+ - name: Create Java function
+ working-directory: ./experimental/aws-lambda-java-profiler
+ run: ./integration_tests/create_function.sh
+
+ - name: Invoke Java function
+ working-directory: ./experimental/aws-lambda-java-profiler
+ run: ./integration_tests/invoke_function.sh
+
+ - name: Download from s3
+ working-directory: ./experimental/aws-lambda-java-profiler
+ run: ./integration_tests/download_from_s3.sh
+
+ - name: Upload profiles
+ uses: actions/upload-artifact@v4
+ with:
+ name: profiles
+ path: /tmp/s3-artifacts
+
+ - name: cleanup
+ if: always()
+ working-directory: ./experimental/aws-lambda-java-profiler
+ run: ./integration_tests/cleanup.sh
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 371bed6b..9f99cc41 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,3 +28,10 @@ dependency-reduced-pom.xml
# snapshot process
aws-lambda-java-runtime-interface-client/pom.xml.versionsBackup
+
+# profiler
+experimental/aws-lambda-java-profiler/integration_tests/helloworld/build
+experimental/aws-lambda-java-profiler/extension/build/
+experimental/aws-lambda-java-profiler/integration_tests/helloworld/bin
+!experimental/aws-lambda-java-profiler/extension/gradle/wrapper/*.jar
+/scratch/
diff --git a/README.md b/README.md
index c72dcf0c..fdc08a75 100644
--- a/README.md
+++ b/README.md
@@ -139,6 +139,18 @@ See the [README](aws-lambda-java-log4j2/README.md) or the [official documentatio
```
+## Lambda Profiler Extension for Java - aws-lambda-java-profiler
+
+
+
+
+
+This project allows you to profile your Java functions invoke by invoke, with high fidelity, and no code changes. It
+uses the [async-profiler](https://github.com/async-profiler/async-profiler) project to produce profiling data and
+automatically uploads the data as flame graphs to S3.
+
+Follow our [Quick Start](experimental/aws-lambda-java-profiler#quick-start) to profile your functions.
+
## Java implementation of the Runtime Interface Client API - aws-lambda-java-runtime-interface-client
[](https://central.sonatype.com/artifact/com.amazonaws/aws-lambda-java-runtime-interface-client)
diff --git a/experimental/aws-lambda-java-profiler/.gitignore b/experimental/aws-lambda-java-profiler/.gitignore
new file mode 100644
index 00000000..4c3fb86d
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/.gitignore
@@ -0,0 +1,3 @@
+*.zip
+/.idea/
+/target/
diff --git a/experimental/aws-lambda-java-profiler/.mvn/wrapper/maven-wrapper.properties b/experimental/aws-lambda-java-profiler/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 00000000..48a56c99
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+wrapperVersion=3.3.2
+distributionType=only-script
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip
diff --git a/experimental/aws-lambda-java-profiler/README.md b/experimental/aws-lambda-java-profiler/README.md
new file mode 100644
index 00000000..aec73f1a
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/README.md
@@ -0,0 +1,113 @@
+
+
+
+
+AWS Lambda Profiler Extension for Java
+
+The Lambda profiler extension allows you to profile your Java functions invoke by invoke, with high fidelity, and no
+code changes. It uses the [async-profiler](https://github.com/async-profiler/async-profiler) project to produce
+profiling data and automatically uploads the data as HTML flame graphs to S3.
+
+
+
+
+
+## Current status
+**This is an alpha release and not yet ready for production use.** We're especially interested in early feedback on usability, features, performance, and compatibility. Please send feedback by opening a [GitHub issue](https://github.com/aws/aws-lambda-java-libs/issues/new).
+
+The profiler has been tested with Lambda managed runtimes for Java 17 and Java 21.
+
+## How to use the Lambda Profiler
+
+To use the profiler you need to
+
+1. Build the extension in this repo
+2. Deploy it as a Lambda Layer and attach the layer to your function
+3. Create an S3 bucket for the results, or reuse an existing one
+4. Give your function permission to write to the bucket
+5. Configure the required environment variables.
+
+The above assumes you're using the ZIP deployment method with managed runtimes. If you deploy your functions as container images instead, you will need to include the profiler in your Dockerfile at `/opt/extensions/` rather than using a Lambda layer.
+
+### Quick Start
+
+The following [Quick Start](#quick-start) gives AWS CLI commands you can run to get started (MacOS/Linux). There are also [examples](examples) using infrastructure as code for you to refer to.
+
+1. Clone the repo
+
+ ```bash
+ git clone https://github.com/aws/aws-lambda-java-libs
+ ```
+
+2. Build the extension
+
+ ```bash
+ cd aws-lambda-java-libs/experimental/aws-lambda-java-profiler/extension
+ ./build_layer.sh
+ ```
+
+3. Run the `update-function.sh` script which will create a new S3 bucket, Lambda layer and all the configuration required.
+
+ ```bash
+ cd ..
+ ./update-function.sh YOUR_FUNCTION_NAME
+ ```
+
+4. Invoke your function and review the flame graph in S3 using your browser.
+
+### Configuration
+
+#### Required Environment Variables
+
+| Name | Value |
+|-----------------------------------------|-----------------------------------------------------------------------------------------------|
+| AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME | Your unique bucket name |
+| JAVA_TOOL_OPTIONS | -XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints -javaagent:/opt/profiler-extension.jar |
+
+#### Optional Environment Variables
+
+| Name | Default Value | Options |
+|------------------------------------------|-----------------------------------------------------------|--------------------------------|
+| AWS_LAMBDA_PROFILER_START_COMMAND | start,event=wall,interval=1us | |
+| AWS_LAMBDA_PROFILER_STOP_COMMAND | stop,file=%s,include=*AWSLambda.main,include=start_thread | file=%s is required |
+| AWS_LAMBDA_PROFILER_DEBUG | false | true - to enable debug logging |
+| AWS_LAMBDA_PROFILER_COMMUNICATION_PORT | 1234 | a valid port number |
+
+### How does it work?
+
+In `/src` is the code for a Java agent. It's entry point `AgentEntry.premain()` is executed as the runtime starts up.
+The environment variable `JAVA_TOOL_OPTIONS` is used to specify which `.jar` file the agent is in. The `MANIFEST.MF` file is used to specify the pre-main class.
+
+When the agent is constructed, it starts the profiler and registers itself as a Lambda extension for `INVOKE` request.
+
+A new thread is created to handle calling `/next` and uploading the results of the profiler to S3. The bucket to upload
+the result to is configurable using an environment variable.
+
+### Troubleshooting
+
+- Ensure the Lambda function execution role has the necessary permissions to write to the S3 bucket.
+- Verify that the environment variables are set correctly in your Lambda function configuration.
+- Check CloudWatch logs for any error messages from the extension.
+
+## Contributing
+
+Contributions to improve the Java profiler extension are welcome. Please see [CONTRIBUTING.md](../../CONTRIBUTING.md) for more information on how to report bugs or submit pull requests.
+
+Issues or contributions to the [async-profiler](https://github.com/async-profiler/async-profiler) itself should be submitted to that project.
+
+### Security
+
+If you discover a potential security issue in this project we ask that you notify AWS Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public GitHub issue.
+
+### Code of conduct
+
+This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). See [CODE_OF_CONDUCT.md](doc/CODE_OF_CONDUCT.md) for more details.
+
+## License
+
+This project is licensed under the [Apache 2.0](../../LICENSE) License. It uses the following projects:
+
+- [async-profiler](https://github.com/async-profiler/async-profiler) (Apache 2.0 license)
+- [AWS SDK for Java 2.0](https://github.com/aws/aws-sdk-java-v2) (Apache 2.0 license)
+- Other libraries in this repository (Apache 2.0 license)
+
diff --git a/experimental/aws-lambda-java-profiler/docs/Arch_AWS-Lambda_64.svg b/experimental/aws-lambda-java-profiler/docs/Arch_AWS-Lambda_64.svg
new file mode 100644
index 00000000..496ef0e7
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/docs/Arch_AWS-Lambda_64.svg
@@ -0,0 +1,18 @@
+
+
+
+ Icon-Architecture/64/Arch_AWS-Lambda_64
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/experimental/aws-lambda-java-profiler/docs/example-cold-start-flame-graph-small.png b/experimental/aws-lambda-java-profiler/docs/example-cold-start-flame-graph-small.png
new file mode 100644
index 00000000..81ae8cba
Binary files /dev/null and b/experimental/aws-lambda-java-profiler/docs/example-cold-start-flame-graph-small.png differ
diff --git a/experimental/aws-lambda-java-profiler/docs/example-cold-start-flame-graph.png b/experimental/aws-lambda-java-profiler/docs/example-cold-start-flame-graph.png
new file mode 100644
index 00000000..26d11c31
Binary files /dev/null and b/experimental/aws-lambda-java-profiler/docs/example-cold-start-flame-graph.png differ
diff --git a/experimental/aws-lambda-java-profiler/examples/cdk/.gitignore b/experimental/aws-lambda-java-profiler/examples/cdk/.gitignore
new file mode 100644
index 00000000..1db21f16
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/examples/cdk/.gitignore
@@ -0,0 +1,13 @@
+.classpath.txt
+target
+.classpath
+.project
+.idea
+.settings
+.vscode
+*.iml
+
+# CDK asset staging directory
+.cdk.staging
+cdk.out
+
diff --git a/experimental/aws-lambda-java-profiler/examples/cdk/README.md b/experimental/aws-lambda-java-profiler/examples/cdk/README.md
new file mode 100644
index 00000000..516ef71a
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/examples/cdk/README.md
@@ -0,0 +1,18 @@
+# Welcome to your CDK Java project!
+
+This is a blank project for CDK development with Java.
+
+The `cdk.json` file tells the CDK Toolkit how to execute your app.
+
+It is a [Maven](https://maven.apache.org/) based project, so you can open this project with any Maven compatible Java IDE to build and run tests.
+
+## Useful commands
+
+ * `mvn package` compile and run tests
+ * `cdk ls` list all stacks in the app
+ * `cdk synth` emits the synthesized CloudFormation template
+ * `cdk deploy` deploy this stack to your default AWS account/region
+ * `cdk diff` compare deployed stack with current state
+ * `cdk docs` open CDK documentation
+
+Enjoy!
diff --git a/experimental/aws-lambda-java-profiler/examples/cdk/cdk.json b/experimental/aws-lambda-java-profiler/examples/cdk/cdk.json
new file mode 100644
index 00000000..e94ff851
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/examples/cdk/cdk.json
@@ -0,0 +1,68 @@
+{
+ "app": "mvn -e -q compile exec:java",
+ "watch": {
+ "include": [
+ "**"
+ ],
+ "exclude": [
+ "README.md",
+ "cdk*.json",
+ "target",
+ "pom.xml",
+ "src/test"
+ ]
+ },
+ "context": {
+ "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
+ "@aws-cdk/core:checkSecretUsage": true,
+ "@aws-cdk/core:target-partitions": [
+ "aws",
+ "aws-cn"
+ ],
+ "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
+ "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
+ "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
+ "@aws-cdk/aws-iam:minimizePolicies": true,
+ "@aws-cdk/core:validateSnapshotRemovalPolicy": true,
+ "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
+ "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
+ "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
+ "@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
+ "@aws-cdk/core:enablePartitionLiterals": true,
+ "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
+ "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
+ "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
+ "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
+ "@aws-cdk/aws-route53-patters:useCertificate": true,
+ "@aws-cdk/customresources:installLatestAwsSdkDefault": false,
+ "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
+ "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
+ "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
+ "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
+ "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
+ "@aws-cdk/aws-redshift:columnId": true,
+ "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
+ "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
+ "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
+ "@aws-cdk/aws-kms:aliasNameRef": true,
+ "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
+ "@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
+ "@aws-cdk/aws-efs:denyAnonymousAccess": true,
+ "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
+ "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
+ "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
+ "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
+ "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
+ "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
+ "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true,
+ "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true,
+ "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true,
+ "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true,
+ "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true,
+ "@aws-cdk/aws-eks:nodegroupNameAttribute": true,
+ "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
+ "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true,
+ "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false,
+ "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false
+ }
+}
diff --git a/experimental/aws-lambda-java-profiler/examples/cdk/pom.xml b/experimental/aws-lambda-java-profiler/examples/cdk/pom.xml
new file mode 100644
index 00000000..01bbf0d6
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/examples/cdk/pom.xml
@@ -0,0 +1,59 @@
+
+
+ 4.0.0
+
+ com.myorg
+ example-cdk-profiler-layer
+ 0.1
+
+
+ UTF-8
+ 2.155.0
+ [10.0.0,11.0.0)
+ 5.7.1
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ 17
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.1.0
+
+ com.myorg.InfraApp
+
+
+
+
+
+
+
+ software.amazon.awscdk
+ aws-cdk-lib
+ ${cdk.version}
+
+
+
+ software.constructs
+ constructs
+ ${constructs.version}
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit.version}
+ test
+
+
+
diff --git a/experimental/aws-lambda-java-profiler/examples/cdk/src/main/java/com/myorg/InfraApp.java b/experimental/aws-lambda-java-profiler/examples/cdk/src/main/java/com/myorg/InfraApp.java
new file mode 100644
index 00000000..1232c1b8
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/examples/cdk/src/main/java/com/myorg/InfraApp.java
@@ -0,0 +1,42 @@
+package com.myorg;
+
+import software.amazon.awscdk.App;
+import software.amazon.awscdk.Environment;
+import software.amazon.awscdk.StackProps;
+
+import java.util.Arrays;
+
+public class InfraApp {
+ public static void main(final String[] args) {
+ App app = new App();
+
+ new InfraStack(app, "InfraStack", StackProps.builder()
+ // If you don't specify 'env', this stack will be environment-agnostic.
+ // Account/Region-dependent features and context lookups will not work,
+ // but a single synthesized template can be deployed anywhere.
+
+ // Uncomment the next block to specialize this stack for the AWS Account
+ // and Region that are implied by the current CLI configuration.
+ /*
+ .env(Environment.builder()
+ .account(System.getenv("CDK_DEFAULT_ACCOUNT"))
+ .region(System.getenv("CDK_DEFAULT_REGION"))
+ .build())
+ */
+
+ // Uncomment the next block if you know exactly what Account and Region you
+ // want to deploy the stack to.
+ /*
+ .env(Environment.builder()
+ .account("123456789012")
+ .region("us-east-1")
+ .build())
+ */
+
+ // For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html
+ .build());
+
+ app.synth();
+ }
+}
+
diff --git a/experimental/aws-lambda-java-profiler/examples/cdk/src/main/java/com/myorg/InfraStack.java b/experimental/aws-lambda-java-profiler/examples/cdk/src/main/java/com/myorg/InfraStack.java
new file mode 100644
index 00000000..79773e39
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/examples/cdk/src/main/java/com/myorg/InfraStack.java
@@ -0,0 +1,53 @@
+package com.myorg;
+
+import software.amazon.awscdk.Duration;
+import software.amazon.awscdk.services.lambda.Code;
+import software.amazon.awscdk.services.lambda.Function;
+import software.amazon.awscdk.services.lambda.LayerVersion;
+import software.amazon.awscdk.services.s3.Bucket;
+import software.constructs.Construct;
+import software.amazon.awscdk.Stack;
+import software.amazon.awscdk.StackProps;
+
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import static software.amazon.awscdk.services.lambda.Architecture.*;
+import static software.amazon.awscdk.services.lambda.Runtime.*;
+
+public class InfraStack extends Stack {
+ public InfraStack(final Construct scope, final String id) {
+ this(scope, id, null);
+ }
+
+ public InfraStack(final Construct scope, final String id, final StackProps props) {
+ super(scope, id, props);
+
+ var resultsBucketName = UUID.randomUUID().toString();
+ var resultsBucket = Bucket.Builder.create(this, "profiler-results-bucket")
+ .bucketName(resultsBucketName)
+ .build();
+
+ var layerVersion = LayerVersion.Builder.create(this, "async-profiler-layer")
+ .compatibleArchitectures(List.of(ARM_64, X86_64))
+ .compatibleRuntimes(List.of(JAVA_11, JAVA_17, JAVA_21))
+ .code(Code.fromAsset("../../target/extension.zip"))
+ .build();
+
+ var environmentVariables = Map.of("JAVA_TOOL_OPTIONS", "-XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints -javaagent:/opt/profiler.jar",
+ "AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME", resultsBucketName);
+
+ var function = Function.Builder.create(this, "example-profiler-function")
+ .runtime(JAVA_21)
+ .handler("helloworld.App")
+ .code(Code.fromAsset("../function/profiling-example/target/Helloworld-1.0.jar"))
+ .memorySize(2048)
+ .layers(List.of(layerVersion))
+ .environment(environmentVariables)
+ .timeout(Duration.seconds(30))
+ .build();
+
+ resultsBucket.grantPut(function);
+ }
+}
diff --git a/experimental/aws-lambda-java-profiler/examples/function/profiling-example/pom.xml b/experimental/aws-lambda-java-profiler/examples/function/profiling-example/pom.xml
new file mode 100644
index 00000000..c7465bfd
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/examples/function/profiling-example/pom.xml
@@ -0,0 +1,63 @@
+
+ 4.0.0
+ helloworld
+ HelloWorld
+ 1.0
+ jar
+ A sample Hello World created for SAM CLI.
+
+ 21
+ 21
+
+
+
+
+ com.amazonaws
+ aws-lambda-java-core
+ 1.2.2
+
+
+ com.amazonaws
+ aws-lambda-java-events
+ 3.11.0
+
+
+ com.hkupty.penna
+ penna-core
+ 0.8.0
+
+
+ org.slf4j
+ slf4j-api
+ 2.0.13
+
+
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.2.4
+
+
+
+
+ package
+
+ shade
+
+
+
+
+
+
+
diff --git a/experimental/aws-lambda-java-profiler/examples/function/profiling-example/src/main/java/helloworld/App.java b/experimental/aws-lambda-java-profiler/examples/function/profiling-example/src/main/java/helloworld/App.java
new file mode 100644
index 00000000..c58f55a1
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/examples/function/profiling-example/src/main/java/helloworld/App.java
@@ -0,0 +1,53 @@
+package helloworld;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestHandler;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handler for requests to Lambda function.
+ */
+public class App implements RequestHandler {
+
+ private static Logger logger = LoggerFactory.getLogger(App.class);
+
+ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
+ Map headers = new HashMap<>();
+ headers.put("Content-Type", "application/json");
+ headers.put("X-Custom-Header", "application/json");
+
+ APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent()
+ .withHeaders(headers);
+ try {
+ final String pageContents = this.getPageContents("https://checkip.amazonaws.com");
+ String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents);
+ logger.info(output);
+
+ return response
+ .withStatusCode(200)
+ .withBody(output);
+ } catch (IOException e) {
+ return response
+ .withBody("{}")
+ .withStatusCode(500);
+ }
+ }
+
+ private String getPageContents(String address) throws IOException{
+ URL url = new URL(address);
+ try(BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()))) {
+ return br.lines().collect(Collectors.joining(System.lineSeparator()));
+ }
+ }
+}
diff --git a/experimental/aws-lambda-java-profiler/examples/function/profiling-example/src/test/java/helloworld/AppTest.java b/experimental/aws-lambda-java-profiler/examples/function/profiling-example/src/test/java/helloworld/AppTest.java
new file mode 100644
index 00000000..240323bb
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/examples/function/profiling-example/src/test/java/helloworld/AppTest.java
@@ -0,0 +1,22 @@
+package helloworld;
+
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import org.junit.Test;
+
+public class AppTest {
+ @Test
+ public void successfulResponse() {
+ App app = new App();
+ APIGatewayProxyResponseEvent result = app.handleRequest(null, null);
+ assertEquals(200, result.getStatusCode().intValue());
+ assertEquals("application/json", result.getHeaders().get("Content-Type"));
+ String content = result.getBody();
+ assertNotNull(content);
+ assertTrue(content.contains("\"message\""));
+ assertTrue(content.contains("\"hello world\""));
+ assertTrue(content.contains("\"location\""));
+ }
+}
diff --git a/experimental/aws-lambda-java-profiler/extension/build.gradle b/experimental/aws-lambda-java-profiler/extension/build.gradle
new file mode 100644
index 00000000..0c8d53e9
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/extension/build.gradle
@@ -0,0 +1,34 @@
+plugins {
+ id 'java'
+ id "com.gradleup.shadow" version "8.3.3"
+}
+
+repositories {
+ mavenCentral()
+}
+
+sourceCompatibility = 11
+targetCompatibility = 11
+
+dependencies {
+ implementation 'com.amazonaws:aws-lambda-java-core:1.2.3'
+ implementation 'com.amazonaws:aws-lambda-java-events:3.11.5'
+ implementation("tools.profiler:async-profiler:3.0")
+ implementation("software.amazon.awssdk:s3:2.28.9") {
+ exclude group: 'software.amazon.awssdk', module: 'netty-nio-client'
+ }
+}
+
+jar {
+ manifest {
+ attributes 'Main-Class': 'com.amazonaws.services.lambda.extension.ExtensionMain'
+ attributes 'Premain-Class': 'com.amazonaws.services.lambda.extension.PreMain'
+ attributes 'Can-Redefine-Class': true
+ }
+}
+
+shadowJar {
+ archiveFileName = "profiler-extension.jar"
+}
+
+build.dependsOn jar
diff --git a/experimental/aws-lambda-java-profiler/extension/build_layer.sh b/experimental/aws-lambda-java-profiler/extension/build_layer.sh
new file mode 100755
index 00000000..cfb381cf
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/extension/build_layer.sh
@@ -0,0 +1,13 @@
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: MIT-0
+
+./gradlew :shadowJar
+
+chmod +x extensions/profiler-extension
+archive="extension.zip"
+if [ -f "$archive" ] ; then
+ rm "$archive"
+fi
+
+zip "$archive" -j build/libs/profiler-extension.jar
+zip "$archive" extensions/*
\ No newline at end of file
diff --git a/experimental/aws-lambda-java-profiler/extension/extensions/profiler-extension b/experimental/aws-lambda-java-profiler/extension/extensions/profiler-extension
new file mode 100755
index 00000000..ef9a5e47
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/extension/extensions/profiler-extension
@@ -0,0 +1,6 @@
+#!/bin/bash
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: MIT-0
+
+set -euo pipefail
+exec -- java -jar /opt/profiler-extension.jar
\ No newline at end of file
diff --git a/experimental/aws-lambda-java-profiler/extension/gradle/wrapper/gradle-wrapper.jar b/experimental/aws-lambda-java-profiler/extension/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..a4b76b95
Binary files /dev/null and b/experimental/aws-lambda-java-profiler/extension/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/experimental/aws-lambda-java-profiler/extension/gradle/wrapper/gradle-wrapper.properties b/experimental/aws-lambda-java-profiler/extension/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..df97d72b
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/extension/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/experimental/aws-lambda-java-profiler/extension/gradlew b/experimental/aws-lambda-java-profiler/extension/gradlew
new file mode 100755
index 00000000..f5feea6d
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/extension/gradlew
@@ -0,0 +1,252 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
+' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/experimental/aws-lambda-java-profiler/extension/gradlew.bat b/experimental/aws-lambda-java-profiler/extension/gradlew.bat
new file mode 100644
index 00000000..9b42019c
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/extension/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ExtensionClient.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ExtensionClient.java
new file mode 100644
index 00000000..60c13a81
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ExtensionClient.java
@@ -0,0 +1,73 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: MIT-0
+package com.amazonaws.services.lambda.extension;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.Optional;
+
+/**
+ * Utility class that takes care of registration of extension, fetching the next event, initializing
+ * and exiting with error
+ */
+public class ExtensionClient {
+ private static final String EXTENSION_NAME = "profiler-extension";
+ private static final String BASEURL = String
+ .format("http://%s/2020-01-01/extension", System.getenv("AWS_LAMBDA_RUNTIME_API"));
+ private static final String BODY = "{" +
+ " \"events\": [" +
+ " \"INVOKE\"," +
+ " \"SHUTDOWN\"" +
+ " ]" +
+ " }";
+ private static final String LAMBDA_EXTENSION_IDENTIFIER = "Lambda-Extension-Identifier";
+ private static final HttpClient client = HttpClient.newBuilder().build();
+
+ public static String registerExtension() {
+ final String registerUrl = String.format("%s/register", BASEURL);
+ HttpRequest request = HttpRequest.newBuilder()
+ .POST(HttpRequest.BodyPublishers.ofString(BODY))
+ .header("Content-Type", "application/json")
+ .header("Lambda-Extension-Name", EXTENSION_NAME)
+ .uri(URI.create(registerUrl))
+ .build();
+ try {
+ HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
+ // Get extension ID from the response headers
+ Optional lambdaExtensionHeader = response.headers().firstValue("lambda-extension-identifier");
+ if (lambdaExtensionHeader.isPresent()) {
+ return lambdaExtensionHeader.get();
+ }
+ }
+ catch (Exception e) {
+ Logger.error("could not register the extension");
+ e.printStackTrace();
+ }
+ throw new RuntimeException("Error while registering extension");
+ }
+
+ public static String getNext(final String extensionId) {
+ try {
+ final String nextEventUrl = String.format("%s/event/next", BASEURL);
+ HttpRequest request = HttpRequest.newBuilder()
+ .GET()
+ .header(LAMBDA_EXTENSION_IDENTIFIER, extensionId)
+ .uri(URI.create(nextEventUrl))
+ .build();
+ HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
+ if (response.statusCode() == 200) {
+ return response.body();
+ } else {
+ Logger.error("invalid status code returned while processing event = " + response.statusCode());
+ }
+ }
+ catch (Exception e) {
+ Logger.error("could not get /next event");
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+}
diff --git a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ExtensionMain.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ExtensionMain.java
new file mode 100644
index 00000000..480025ad
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ExtensionMain.java
@@ -0,0 +1,136 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: MIT-0
+package com.amazonaws.services.lambda.extension;
+
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.URI;
+import java.util.UUID;
+
+public class ExtensionMain {
+
+ private static final HttpClient client = HttpClient.newBuilder().build();
+ private static String previousFileSuffix = null;
+ private static boolean coldstart = true;
+ private static final String REQUEST_ID = "requestId";
+ private static final String EVENT_TYPE = "eventType";
+ private static final String INTERNAL_COMMUNICATION_PORT = System.getenv().getOrDefault("AWS_LAMBDA_PROFILER_COMMUNICATION_PORT", "1234");
+ public static final String HEADER_NAME = "X-FileName";
+
+ private static S3Manager s3Manager;
+
+ public static void main(String[] args) {
+ final String extension = ExtensionClient.registerExtension();
+ Logger.debug("Extension registration complete, extensionID: " + extension);
+ s3Manager = new S3Manager();
+ while (true) {
+ try {
+ String response = ExtensionClient.getNext(extension);
+ if (response != null && !response.isEmpty()) {
+ final String eventType = extractInfo(EVENT_TYPE, response);
+ Logger.debug("eventType = " + eventType);
+ if (eventType != null) {
+ switch (eventType) {
+ case "INVOKE":
+ handleInvoke(response);
+ break;
+ case "SHUTDOWN":
+ handleShutDown();
+ break;
+ default:
+ Logger.error("invalid event type received " + eventType);
+ }
+ }
+ }
+ } catch (Exception e) {
+ Logger.error("error while processing extension -" + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private static void handleShutDown() {
+ Logger.debug("handling SHUTDOWN event, flushing the last profile");
+ try {
+ // no need to stop the profiler as it has been stopped by the shutdown hook
+ s3Manager.upload(previousFileSuffix, true);
+ } catch (Exception e) {
+ Logger.error("could not start the profiler");
+ throw e;
+ }
+ System.exit(0);
+ }
+
+ public static void handleInvoke(String payload) {
+ final String requestId = extractInfo(REQUEST_ID, payload);
+ final String randomSuffix = UUID.randomUUID().toString().substring(0,5);
+ Logger.debug("handling INVOKE event, requestID = " + requestId);
+ if (!coldstart) {
+ try {
+ stopProfiler(previousFileSuffix);
+ s3Manager.upload(previousFileSuffix, false);
+ startProfiler();
+ } catch (Exception e) {
+ Logger.error("could not start the profiler");
+ throw e;
+ }
+ }
+ coldstart = false;
+ previousFileSuffix = extractInfo(REQUEST_ID, payload) + "-" + randomSuffix;
+ }
+
+ private static String extractInfo(String info, String jsonString) {
+ String prefix = "\"" + info + "\":\"";
+ String suffix = "\"";
+
+ int startIndex = jsonString.indexOf(prefix);
+ if (startIndex == -1) {
+ return null; // requestId not found
+ }
+
+ startIndex += prefix.length();
+ int endIndex = jsonString.indexOf(suffix, startIndex);
+
+ if (endIndex == -1) {
+ return null; // Malformed JSON
+ }
+
+ return jsonString.substring(startIndex, endIndex);
+ }
+
+ private static void startProfiler() {
+ try {
+ String url = String.format("http://localhost:%s/profiler/start", INTERNAL_COMMUNICATION_PORT);
+ HttpRequest request = HttpRequest.newBuilder()
+ .GET()
+ .uri(URI.create(url))
+ .build();
+ HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
+ if (response.statusCode() == 200) {
+ Logger.debug("profiler successfully started");
+ }
+ } catch(Exception e) {
+ Logger.error("could not start the profiler");
+ e.printStackTrace();
+ }
+ }
+
+ private static void stopProfiler(String fileNameSuffix) {
+ try {
+ String url = String.format("http://localhost:%s/profiler/stop", INTERNAL_COMMUNICATION_PORT);
+ HttpRequest request = HttpRequest.newBuilder()
+ .GET()
+ .setHeader(HEADER_NAME, fileNameSuffix)
+ .uri(URI.create(url))
+ .build();
+ HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
+ if (response.statusCode() == 200) {
+ Logger.debug("profiler successfully stopped");
+ }
+ } catch(Exception e) {
+ Logger.error("could not stop the profiler");
+ e.printStackTrace();
+ }
+ }
+}
\ No newline at end of file
diff --git a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/Logger.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/Logger.java
new file mode 100644
index 00000000..e064da10
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/Logger.java
@@ -0,0 +1,25 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: MIT-0
+package com.amazonaws.services.lambda.extension;
+
+public class Logger {
+
+ private static final boolean IS_DEBUG_ENABLED = initializeDebugFlag();
+ private static final String PREFIX = "[PROFILER] ";
+
+ private static boolean initializeDebugFlag() {
+ String envValue = System.getenv("AWS_LAMBDA_PROFILER_DEBUG");
+ return "true".equalsIgnoreCase(envValue) || "1".equals(envValue);
+ }
+
+ public static void debug(String message) {
+ if(IS_DEBUG_ENABLED) {
+ System.out.println(PREFIX + message);
+ }
+ }
+
+ public static void error(String message) {
+ System.out.println(PREFIX + message);
+ }
+
+}
\ No newline at end of file
diff --git a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/PreMain.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/PreMain.java
new file mode 100644
index 00000000..4e968eb7
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/PreMain.java
@@ -0,0 +1,110 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: MIT-0
+package com.amazonaws.services.lambda.extension;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.instrument.Instrumentation;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+
+import one.profiler.AsyncProfiler;
+
+public class PreMain {
+
+ private static final String DEFAULT_AWS_LAMBDA_PROFILER_START_COMMAND = "start,event=wall,interval=1us";
+ private static final String DEFAULT_AWS_LAMBDA_PROFILER_STOP_COMMAND = "stop,file=%s,include=*AWSLambda.main,include=start_thread";
+ private static final String PROFILER_START_COMMAND = System.getenv().getOrDefault("AWS_LAMBDA_PROFILER_START_COMMAND", DEFAULT_AWS_LAMBDA_PROFILER_START_COMMAND);
+ private static final String PROFILER_STOP_COMMAND = System.getenv().getOrDefault("AWS_LAMBDA_PROFILER_STOP_COMMAND", DEFAULT_AWS_LAMBDA_PROFILER_STOP_COMMAND);
+ private static final String INTERNAL_COMMUNICATION_PORT = System.getenv().getOrDefault("AWS_LAMBDA_PROFILER_COMMUNICATION_PORT", "1234");
+
+ public static void premain(String agentArgs, Instrumentation inst) {
+ Logger.debug("premain is starting");
+ if(!createFileIfNotExist("/tmp/aws-lambda-java-profiler")) {
+ Logger.debug("starting the profiler for coldstart");
+ startProfiler();
+ registerShutdownHook(AsyncProfiler.getInstance());
+ try {
+ Integer port = Integer.parseInt(INTERNAL_COMMUNICATION_PORT);
+ Logger.debug("using profile communication port = " + port);
+ HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
+ server.createContext("/profiler/start", new StartProfiler());
+ server.createContext("/profiler/stop", new StopProfiler());
+ server.setExecutor(null); // Use the default executor
+ server.start();
+ } catch(Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private static boolean createFileIfNotExist(String filePath) {
+ File file = new File(filePath);
+ try {
+ return file.createNewFile();
+ } catch (IOException e) {
+ System.out.println(e);
+ return false;
+ }
+ }
+
+ public static class StopProfiler implements HttpHandler {
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ Logger.debug("hit /profiler/stop");
+ final String fileName = exchange.getRequestHeaders().getFirst(ExtensionMain.HEADER_NAME);
+ stopProfiler(fileName);
+ String response = "ok";
+ exchange.sendResponseHeaders(200, response.length());
+ try (OutputStream os = exchange.getResponseBody()) {
+ os.write(response.getBytes(StandardCharsets.UTF_8));
+ }
+ }
+ }
+
+ public static class StartProfiler implements HttpHandler {
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ Logger.debug("hit /profiler/start");
+ startProfiler();
+ String response = "ok";
+ exchange.sendResponseHeaders(200, response.length());
+ try (OutputStream os = exchange.getResponseBody()) {
+ os.write(response.getBytes(StandardCharsets.UTF_8));
+ }
+ }
+ }
+
+
+ public static void stopProfiler(String fileNameSuffix) {
+ try {
+ final String fileName = String.format("/tmp/profiling-data-%s.html", fileNameSuffix);
+ Logger.debug("stopping the profiler with filename = " + fileName + " with command = " + PROFILER_STOP_COMMAND);
+ AsyncProfiler.getInstance().execute(String.format(PROFILER_STOP_COMMAND, fileName));
+ } catch(Exception e) {
+ Logger.error("could not stop the profiler");
+ e.printStackTrace();
+ }
+ }
+
+ public static void startProfiler() {
+ try {
+ Logger.debug("staring the profiler with command = " + PROFILER_START_COMMAND);
+ AsyncProfiler.getInstance().execute(PROFILER_START_COMMAND);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static void registerShutdownHook(AsyncProfiler profiler) {
+ Logger.debug("registering shutdown hook");
+ Thread shutdownHook = new Thread(new ShutdownHook(profiler));
+ Runtime.getRuntime().addShutdownHook(shutdownHook);
+ }
+
+}
\ No newline at end of file
diff --git a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/S3Manager.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/S3Manager.java
new file mode 100644
index 00000000..3b55984c
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/S3Manager.java
@@ -0,0 +1,67 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: MIT-0
+package com.amazonaws.services.lambda.extension;
+
+import java.io.File;
+import java.time.format.DateTimeFormatter;
+import java.time.Instant;
+import java.time.LocalDate;
+
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
+import software.amazon.awssdk.services.s3.model.PutObjectResponse;
+
+public class S3Manager {
+
+ private static final String RESULTS_BUCKET = "AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME";
+ private static final String FUNCTION_NAME = System.getenv().getOrDefault("AWS_LAMBDA_FUNCTION_NAME", "function");
+ private S3Client s3Client;
+ private String bucketName;
+
+ public S3Manager() {
+ final String bucketName = System.getenv(RESULTS_BUCKET);
+ Logger.debug("creating S3Manager with bucketName = " + bucketName);
+ if (null == bucketName || bucketName.isEmpty()) {
+ throw new IllegalArgumentException("please set the bucket name using AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME environment variable");
+ }
+ this.s3Client = S3Client.builder().build();
+ this.bucketName = bucketName;
+ Logger.debug("S3Manager successfully created");
+ }
+
+ public void upload(String fileName, boolean isShutDownEvent) {
+ try {
+ final String suffix = isShutDownEvent ? "shutdown" : fileName;
+ final String key = buildKey(FUNCTION_NAME, fileName);
+ Logger.debug("uploading profile to key = " + key);
+ PutObjectRequest putObjectRequest = PutObjectRequest.builder()
+ .bucket(bucketName)
+ .key(key)
+ .build();
+ File file = new File(String.format("/tmp/profiling-data-%s.html", suffix));
+ if (file.exists()) {
+ Logger.debug("file size is " + file.length());
+ RequestBody requestBody = RequestBody.fromFile(file);
+ PutObjectResponse response = s3Client.putObject(putObjectRequest, requestBody);
+ Logger.debug("profile uploaded successfully. ETag: " + response.eTag());
+ if(file.delete()) {
+ Logger.debug("file deleted");
+ }
+ } else {
+ throw new IllegalArgumentException("could not find the profile to upload");
+ }
+ } catch (Exception e) {
+ Logger.error("could not upload the profile");
+ e.printStackTrace();
+ }
+ }
+
+ private String buildKey(String functionName, String fileName) {
+ final LocalDate currentDate = LocalDate.now();
+ final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
+ final String formattedDate = currentDate.format(formatter);
+ return String.format("%s/%s/%s", formattedDate, functionName, fileName);
+ }
+
+}
\ No newline at end of file
diff --git a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ShutdownHook.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ShutdownHook.java
new file mode 100644
index 00000000..d3a0fa5b
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ShutdownHook.java
@@ -0,0 +1,26 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: MIT-0
+package com.amazonaws.services.lambda.extension;
+
+import one.profiler.AsyncProfiler;
+
+public class ShutdownHook implements Runnable {
+
+ private AsyncProfiler profiler;
+
+ public ShutdownHook(AsyncProfiler profiler) {
+ this.profiler = profiler;
+ }
+
+ @Override
+ public void run() {
+ Logger.debug("running ShutdownHook");
+ try {
+ final String fileName = "/tmp/profiling-data-shutdown.html";
+ Logger.debug("stopping the profiler");
+ AsyncProfiler.getInstance().execute(String.format("stop,file=%s,include=*AWSLambda.main,include=start_thread", fileName));
+ } catch (Exception e) {
+ Logger.error("could not stop the profiler");
+ }
+ }
+}
\ No newline at end of file
diff --git a/experimental/aws-lambda-java-profiler/integration_tests/cleanup.sh b/experimental/aws-lambda-java-profiler/integration_tests/cleanup.sh
new file mode 100755
index 00000000..d58142a0
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/integration_tests/cleanup.sh
@@ -0,0 +1,45 @@
+#!/bin/bash
+
+# Set variables
+LAYER_ARN=$(cat /tmp/layer_arn)
+FUNCTION_NAME="aws-lambda-java-profiler-function-${GITHUB_RUN_ID}"
+ROLE_NAME="aws-lambda-java-profiler-role-${GITHUB_RUN_ID}"
+
+# Function to check if a command was successful
+check_success() {
+ if [ $? -eq 0 ]; then
+ echo "Success: $1"
+ else
+ echo "Error: Failed to $1"
+ exit 1
+ fi
+}
+
+# Delete Lambda Layer
+echo "Deleting Lambda Layer..."
+aws lambda delete-layer-version --layer-name $(echo $LAYER_ARN | cut -d: -f7) --version-number $(echo $LAYER_ARN | cut -d: -f8)
+check_success "delete Lambda Layer"
+
+# Delete Lambda Function
+echo "Deleting Lambda Function..."
+aws lambda delete-function --function-name $FUNCTION_NAME
+check_success "delete Lambda Function"
+
+# Delete IAM Role
+echo "Deleting IAM Role..."
+# First, detach all policies from the role
+for policy in $(aws iam list-attached-role-policies --role-name $ROLE_NAME --query 'AttachedPolicies[*].PolicyArn' --output text); do
+ aws iam detach-role-policy --role-name $ROLE_NAME --policy-arn $policy
+ check_success "detach policy $policy from role $ROLE_NAME"
+done
+
+# Remove s3 inline policy
+aws iam delete-role-policy --role-name $ROLE_NAME --policy-name "s3PutObject"
+check_success "deleted inline policy"
+
+
+# Then delete the role
+aws iam delete-role --role-name $ROLE_NAME
+check_success "delete IAM Role"
+
+echo "All deletions completed successfully."
\ No newline at end of file
diff --git a/experimental/aws-lambda-java-profiler/integration_tests/create_bucket.sh b/experimental/aws-lambda-java-profiler/integration_tests/create_bucket.sh
new file mode 100755
index 00000000..0ba50b73
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/integration_tests/create_bucket.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+PROFILER_RESULTS_BUCKET_NAME="aws-lambda-java-profiler-bucket-${GITHUB_RUN_ID}"
+
+# Create the S3 bucket
+aws s3 mb s3://"$PROFILER_RESULTS_BUCKET_NAME"
+
+# Check if the bucket was created successfully
+if [ $? -eq 0 ]; then
+ echo "Bucket '$PROFILER_RESULTS_BUCKET_NAME' created successfully."
+else
+ echo "Error: Failed to create bucket '$PROFILER_RESULTS_BUCKET_NAME'."
+ exit 1
+fi
\ No newline at end of file
diff --git a/experimental/aws-lambda-java-profiler/integration_tests/create_function.sh b/experimental/aws-lambda-java-profiler/integration_tests/create_function.sh
new file mode 100755
index 00000000..114909d0
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/integration_tests/create_function.sh
@@ -0,0 +1,69 @@
+#!/bin/bash
+
+# Set variables
+FUNCTION_NAME="aws-lambda-java-profiler-function-${GITHUB_RUN_ID}"
+ROLE_NAME="aws-lambda-java-profiler-role-${GITHUB_RUN_ID}"
+HANDLER="helloworld.Handler::handleRequest"
+RUNTIME="java21"
+LAYER_ARN=$(cat /tmp/layer_arn)
+
+JAVA_TOOL_OPTIONS="-XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints -javaagent:/opt/profiler-extension.jar"
+AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME="aws-lambda-java-profiler-bucket-${GITHUB_RUN_ID}"
+
+# Compile the Hello World project
+cd integration_tests/helloworld
+gradle :buildZip
+cd ../..
+
+# Create IAM role for Lambda
+ROLE_ARN=$(aws iam create-role \
+ --role-name $ROLE_NAME \
+ --assume-role-policy-document '{"Version": "2012-10-17","Statement": [{ "Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"}]}' \
+ --query 'Role.Arn' \
+ --output text)
+
+# Attach basic Lambda execution policy to the role
+aws iam attach-role-policy \
+ --role-name $ROLE_NAME \
+ --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
+
+# Attach s3:PutObject policy to the role so we can upload profiles
+POLICY_DOCUMENT=$(cat < $new_filename"
+ else
+ echo "No change: $filename"
+ fi
+ fi
+done
+
+echo "All files processed."
\ No newline at end of file
diff --git a/experimental/aws-lambda-java-profiler/integration_tests/helloworld/build.gradle b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/build.gradle
new file mode 100644
index 00000000..927317f8
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/build.gradle
@@ -0,0 +1,27 @@
+apply plugin: 'java'
+
+repositories {
+ mavenCentral()
+}
+
+sourceCompatibility = 21
+targetCompatibility = 21
+
+dependencies {
+ implementation (
+ 'com.amazonaws:aws-lambda-java-core:1.2.3',
+ 'com.amazonaws:aws-lambda-java-events:3.11.0',
+ 'org.slf4j:slf4j-api:2.0.13'
+ )
+}
+
+task buildZip(type: Zip) {
+ archiveBaseName = "code"
+ from compileJava
+ from processResources
+ into('lib') {
+ from configurations.runtimeClasspath
+ }
+}
+
+build.dependsOn buildZip
\ No newline at end of file
diff --git a/experimental/aws-lambda-java-profiler/integration_tests/helloworld/src/main/java/helloworld/Handler.java b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/src/main/java/helloworld/Handler.java
new file mode 100644
index 00000000..a29cae18
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/src/main/java/helloworld/Handler.java
@@ -0,0 +1,53 @@
+package helloworld;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+
+import java.util.stream.Collectors;
+import java.util.ArrayList;
+import java.util.List;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
+import com.amazonaws.services.lambda.runtime.Context;
+
+import com.amazonaws.services.lambda.runtime.RequestHandler;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Handler implements RequestHandler {
+
+ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
+ long start = System.currentTimeMillis();
+ List result = slowRecursiveFunction(0, 5);
+ long end = System.currentTimeMillis();
+ long duration = end - start;
+
+ System.out.println("Function execution time: " + duration + " ms");
+ System.out.println("Result size: " + result.size());
+ System.out.println("First few elements: " + result.subList(0, Math.min(10, result.size())));
+
+ return new APIGatewayProxyResponseEvent()
+ .withStatusCode(200)
+ .withBody("ok");
+
+ }
+
+ private static List slowRecursiveFunction(int n, int depth) {
+ List result = new ArrayList<>();
+ if (depth == 0) {
+ return result;
+ }
+ long startTime = System.currentTimeMillis();
+ while (System.currentTimeMillis() - startTime < 100) {
+ // nothing to do here
+ }
+ result.add(n);
+ result.addAll(slowRecursiveFunction(n + 2, depth - 1));
+ return result;
+ }
+}
diff --git a/experimental/aws-lambda-java-profiler/integration_tests/helloworld/src/main/resources/wrapper.sh b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/src/main/resources/wrapper.sh
new file mode 100644
index 00000000..b54b7767
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/src/main/resources/wrapper.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+# the path to the interpreter and all of the originally intended arguments
+args=("$@")
+
+# the extra options to pass to the interpreter
+echo "${args[@]}"
+
+# start the runtime with the extra options
+exec "${args[@]}"
\ No newline at end of file
diff --git a/experimental/aws-lambda-java-profiler/integration_tests/invoke_function.sh b/experimental/aws-lambda-java-profiler/integration_tests/invoke_function.sh
new file mode 100755
index 00000000..741eec14
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/integration_tests/invoke_function.sh
@@ -0,0 +1,74 @@
+#!/bin/bash
+
+# Set variables
+FUNCTION_NAME="aws-lambda-java-profiler-function-${GITHUB_RUN_ID}"
+PAYLOAD='{"key": "value"}'
+
+echo "Invoking Lambda function: $FUNCTION_NAME"
+
+# Invoke the Lambda function synchronously and capture the response
+RESPONSE=$(aws lambda invoke \
+ --function-name "$FUNCTION_NAME" \
+ --payload "$PAYLOAD" \
+ --cli-binary-format raw-in-base64-out \
+ --log-type Tail \
+ output.json)
+
+# Extract the status code and log result from the response
+STATUS_CODE=$(echo "$RESPONSE" | jq -r '.StatusCode')
+LOG_RESULT=$(echo "$RESPONSE" | jq -r '.LogResult')
+
+echo "Function invocation completed with status code: $STATUS_CODE"
+
+# Decode and display the logs
+if [ -n "$LOG_RESULT" ]; then
+ echo "Function logs:"
+ echo "$LOG_RESULT" | base64 --decode
+else
+ echo "No logs available."
+fi
+
+# Display the function output
+echo "Function output:"
+cat output.json
+
+echo "$LOG_RESULT" | base64 --decode | grep "starting the profiler for coldstart" || exit 1
+echo "$LOG_RESULT" | base64 --decode | grep -v "uploading" || exit 1
+
+# Clean up the output file
+rm output.json
+
+
+# Invoke it a second time for warm start
+echo "Invoking Lambda function: $FUNCTION_NAME"
+
+# Invoke the Lambda function synchronously and capture the response
+RESPONSE=$(aws lambda invoke \
+ --function-name "$FUNCTION_NAME" \
+ --payload "$PAYLOAD" \
+ --cli-binary-format raw-in-base64-out \
+ --log-type Tail \
+ output.json)
+
+# Extract the status code and log result from the response
+STATUS_CODE=$(echo "$RESPONSE" | jq -r '.StatusCode')
+LOG_RESULT=$(echo "$RESPONSE" | jq -r '.LogResult')
+
+echo "Function invocation completed with status code: $STATUS_CODE"
+
+# Decode and display the logs
+if [ -n "$LOG_RESULT" ]; then
+ echo "Function logs:"
+ echo "$LOG_RESULT" | base64 --decode
+else
+ echo "No logs available."
+fi
+
+# Display the function output
+echo "Function output:"
+cat output.json
+
+echo "$LOG_RESULT" | base64 --decode | grep "uploading" || exit 1
+
+# Clean up the output file
+rm output.json
diff --git a/experimental/aws-lambda-java-profiler/integration_tests/publish_layer.sh b/experimental/aws-lambda-java-profiler/integration_tests/publish_layer.sh
new file mode 100755
index 00000000..879944e8
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/integration_tests/publish_layer.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+
+# Set variables
+LAYER_NAME="aws-lambda-java-profiler-test"
+DESCRIPTION="AWS Lambda Java Profiler Test Layer"
+ZIP_FILE="./extension/extension.zip"
+RUNTIME="java21"
+ARCHITECTURE="x86_64"
+
+# Check if AWS CLI is installed
+if ! command -v aws &> /dev/null; then
+ echo "AWS CLI is not installed. Please install it first."
+ exit 1
+fi
+
+# Check if the ZIP file exists
+if [ ! -f "$ZIP_FILE" ]; then
+ echo "ZIP file $ZIP_FILE not found. Please make sure it exists."
+ exit 1
+fi
+
+# Publish the layer
+echo "Publishing layer $LAYER_NAME..."
+RESPONSE=$(aws lambda publish-layer-version \
+ --layer-name "$LAYER_NAME" \
+ --description "$DESCRIPTION" \
+ --zip-file "fileb://$ZIP_FILE" \
+ --compatible-runtimes "$RUNTIME" \
+ --compatible-architectures "$ARCHITECTURE")
+
+# Check if the layer was published successfully
+if [ $? -eq 0 ]; then
+ LAYER_VERSION=$(echo $RESPONSE | jq -r '.Version')
+ LAYER_ARN=$(echo $RESPONSE | jq -r '.LayerVersionArn')
+ echo "Layer published successfully!"
+ echo "Layer Version: $LAYER_VERSION"
+ echo "Layer ARN: $LAYER_ARN"
+ echo $LAYER_ARN > /tmp/layer_arn
+else
+ echo "Failed to publish layer. Please check your AWS credentials and permissions."
+ exit 1
+fi
\ No newline at end of file
diff --git a/experimental/aws-lambda-java-profiler/update-function.sh b/experimental/aws-lambda-java-profiler/update-function.sh
new file mode 100755
index 00000000..b5fda6ee
--- /dev/null
+++ b/experimental/aws-lambda-java-profiler/update-function.sh
@@ -0,0 +1,76 @@
+#!/bin/bash
+
+# Check if a function name was provided
+if [ $# -eq 0 ]; then
+ echo "Please provide a function name as an argument."
+ echo "Usage: $0 "
+ exit 1
+fi
+
+FUNCTION_NAME="$1"
+
+# Generate a random lowercase S3 bucket name
+RANDOM_SUFFIX=$(uuidgen | tr '[:upper:]' '[:lower:]' | cut -d'-' -f1)
+BUCKET_NAME="my-bucket-${RANDOM_SUFFIX}"
+echo "Generated bucket name: $BUCKET_NAME"
+
+# Create the S3 bucket with the random name
+aws s3 mb "s3://$BUCKET_NAME"
+
+# Create a Lambda layer
+aws lambda publish-layer-version \
+ --layer-name profiler-layer \
+ --description "Profiler Layer" \
+ --license-info "MIT" \
+ --zip-file fileb://extension/extension.zip \
+ --compatible-runtimes java11 java17 java21 \
+ --compatible-architectures "arm64" "x86_64"
+
+# Assign the layer to the function
+aws lambda update-function-configuration \
+ --function-name "$FUNCTION_NAME" \
+ --layers $(aws lambda list-layer-versions --layer-name profiler-layer --query 'LayerVersions[0].LayerVersionArn' --output text)
+
+# Wait for the function to be updated
+aws lambda wait function-updated \
+ --function-name "$FUNCTION_NAME"
+
+# Add environment variables
+aws lambda update-function-configuration \
+ --function-name "$FUNCTION_NAME" \
+ --environment "Variables={AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME=$BUCKET_NAME, JAVA_TOOL_OPTIONS=-XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints -javaagent:/opt/profiler-extension.jar}"
+
+# Update the function's permissions to write to the S3 bucket
+# Get the function's execution role
+ROLE_NAME=$(aws lambda get-function --function-name "$FUNCTION_NAME" --query 'Configuration.Role' --output text | awk -F'/' '{print $NF}')
+
+# Create a policy document
+cat << EOF > s3-write-policy.json
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:PutObject"
+ ],
+ "Resource": [
+ "arn:aws:s3:::$BUCKET_NAME",
+ "arn:aws:s3:::$BUCKET_NAME/*"
+ ]
+ }
+ ]
+}
+EOF
+
+# Attach the policy to the role
+aws iam put-role-policy \
+ --role-name "$ROLE_NAME" \
+ --policy-name S3WriteAccess \
+ --policy-document file://s3-write-policy.json
+
+echo "Setup completed for function $FUNCTION_NAME with S3 bucket $BUCKET_NAME"
+echo "S3 write permissions added to the function's execution role"
+
+# Clean up temporary files
+rm s3-write-policy.json
\ No newline at end of file