From 01b347a48444ead45e1c9de60c6fec05049db19d Mon Sep 17 00:00:00 2001 From: architec <32494274+architec@users.noreply.github.com> Date: Sat, 21 Jun 2025 01:19:37 -0700 Subject: [PATCH 1/5] feat(typescript): add AWS Batch with ECR and Lambda example for OpenMP benchmarks --- typescript/batch-ecr-openmp/README.md | 264 +++++++++ .../bin/aws-batch-openmp-benchmark.ts | 20 + typescript/batch-ecr-openmp/cdk.json | 97 ++++ typescript/batch-ecr-openmp/docker/Dockerfile | 51 ++ typescript/batch-ecr-openmp/jest.config.js | 8 + .../lib/aws-batch-openmp-benchmark-stack.ts | 320 +++++++++++ typescript/batch-ecr-openmp/package.json | 32 ++ .../scripts/build-and-deploy.sh | 117 ++++ .../batch-ecr-openmp/scripts/submit-job.sh | 319 +++++++++++ .../batch-ecr-openmp/scripts/test-local.sh | 117 ++++ .../batch-ecr-openmp/src/openmp/Makefile | 46 ++ .../batch-ecr-openmp/src/openmp/main.cpp | 534 ++++++++++++++++++ .../src/openmp/openmp_benchmark | Bin 0 -> 60720 bytes .../src/openmp/test_benchmark | Bin 0 -> 58672 bytes .../src/openmp/test_benchmark.cpp | 438 ++++++++++++++ .../test/aws-batch-openmp-benchmark.test.ts | 355 ++++++++++++ typescript/batch-ecr-openmp/tsconfig.json | 31 + 17 files changed, 2749 insertions(+) create mode 100644 typescript/batch-ecr-openmp/README.md create mode 100644 typescript/batch-ecr-openmp/bin/aws-batch-openmp-benchmark.ts create mode 100644 typescript/batch-ecr-openmp/cdk.json create mode 100644 typescript/batch-ecr-openmp/docker/Dockerfile create mode 100644 typescript/batch-ecr-openmp/jest.config.js create mode 100644 typescript/batch-ecr-openmp/lib/aws-batch-openmp-benchmark-stack.ts create mode 100644 typescript/batch-ecr-openmp/package.json create mode 100644 typescript/batch-ecr-openmp/scripts/build-and-deploy.sh create mode 100644 typescript/batch-ecr-openmp/scripts/submit-job.sh create mode 100644 typescript/batch-ecr-openmp/scripts/test-local.sh create mode 100644 typescript/batch-ecr-openmp/src/openmp/Makefile create mode 100644 typescript/batch-ecr-openmp/src/openmp/main.cpp create mode 100644 typescript/batch-ecr-openmp/src/openmp/openmp_benchmark create mode 100644 typescript/batch-ecr-openmp/src/openmp/test_benchmark create mode 100644 typescript/batch-ecr-openmp/src/openmp/test_benchmark.cpp create mode 100644 typescript/batch-ecr-openmp/test/aws-batch-openmp-benchmark.test.ts create mode 100644 typescript/batch-ecr-openmp/tsconfig.json diff --git a/typescript/batch-ecr-openmp/README.md b/typescript/batch-ecr-openmp/README.md new file mode 100644 index 000000000..11b51c1d4 --- /dev/null +++ b/typescript/batch-ecr-openmp/README.md @@ -0,0 +1,264 @@ +# AWS Batch with ECR and Lambda + +--- + +![Stability: Stable](https://img.shields.io/badge/stability-Stable-success.svg?style=for-the-badge) + +> **This is a stable example. It should successfully build out of the box** +> +> This examples is built on Construct Libraries marked "Stable" and does not have any infrastructure prerequisites to build. + +--- + + +This example demonstrates how to use AWS Batch with a containerized C++ OpenMP application stored in Amazon ECR, with optional Lambda function for job submission. The OpenMP application performs parallel computing benchmarks (arithmetic operations, mathematical functions, matrix multiplication) to demonstrate CPU-intensive workloads and compare sequential vs parallel execution performance. + +## Prerequisites + +Before deploying, ensure you have: +- AWS CLI configured with appropriate credentials +- Docker installed and running +- Node.js and npm installed +- `jq` command-line JSON processor (for job submission script) + +## Quick Start + +For a complete end-to-end deployment and testing: + +```bash +# 1. Test locally first (optional but recommended) +./scripts/test-local.sh + +# 2. Deploy everything to AWS +AWS_PROFILE=your-profile ./scripts/build-and-deploy.sh + +# 3. Submit a benchmark job +./scripts/submit-job.sh --benchmark-type simple +``` + +## Build + +To build this app manually, you need to be in this example's root folder. Then run the following: + +```bash +npm install -g aws-cdk +npm install +npm run build +``` + +This will install the necessary CDK, then this example's dependencies, and then build your TypeScript files and your CloudFormation template. + +## Deploy + +### Option 1: Automated Deployment (Recommended) + +Use the provided script for a complete deployment: + +```bash +AWS_PROFILE=your-profile ./scripts/build-and-deploy.sh +``` + +This script will: +1. Build the CDK stack +2. Deploy the infrastructure to AWS +3. Build and push the Docker image to ECR +4. Save deployment information for job submission + +### Option 2: Manual Deployment + +For manual control over each step: + +```bash +# Deploy CDK stack +npx cdk deploy --profile your-profile + +# Get ECR login and build/push Docker image +aws ecr get-login-password --region your-region | docker login --username AWS --password-stdin your-account.dkr.ecr.your-region.amazonaws.com +docker build -f docker/Dockerfile -t openmp-benchmark:latest . +docker tag openmp-benchmark:latest your-account.dkr.ecr.your-region.amazonaws.com/openmp-benchmark:latest +docker push your-account.dkr.ecr.your-region.amazonaws.com/openmp-benchmark:latest +``` + +After deployment, you will see the Stack outputs, which include the ECR repository URI and Batch job queue name. + +## Running Benchmarks + +After successful deployment, you can submit OpenMP benchmark jobs using the provided script: + +### Basic Usage + +```bash +# Submit a simple benchmark (default) +./scripts/submit-job.sh + +# Submit specific benchmark types +./scripts/submit-job.sh --benchmark-type math +./scripts/submit-job.sh --benchmark-type matrix +./scripts/submit-job.sh --benchmark-type heavy +./scripts/submit-job.sh --benchmark-type all +``` + +### Advanced Options + +```bash +# Custom parameters +./scripts/submit-job.sh --benchmark-type matrix --instance c6i.2xlarge --threads 8 + +# Submit via Lambda function +./scripts/submit-job.sh --method lambda --benchmark-type simple + +# Preview commands without executing +./scripts/submit-job.sh --dry-run --benchmark-type all +``` + +### Available Benchmark Types + +- **simple** - Basic arithmetic operations (~2 seconds) +- **math** - Mathematical functions: sin, cos, sqrt, pow (~5 seconds) +- **matrix** - Matrix multiplication (memory intensive, ~3 seconds) +- **heavy** - Complex arithmetic expressions (~3 seconds) +- **all** - Run all benchmarks sequentially (~15 seconds total) + +### Monitoring Jobs + +After submitting a job, you can monitor it through: + +1. **AWS Console**: + - Batch: https://console.aws.amazon.com/batch/ + - CloudWatch Logs: https://console.aws.amazon.com/cloudwatch/ + +2. **AWS CLI**: + ```bash + # Check job status + aws batch describe-jobs --profile your-profile --jobs + + # View logs + aws logs describe-log-streams --profile your-profile --log-group-name /aws/batch/openmp-benchmark + ``` + +## Testing + +### Local Testing + +Before deploying to AWS, you can test the OpenMP application locally: + +```bash +./scripts/test-local.sh +``` + +This script will: +1. Build the OpenMP application locally +2. Run C++ unit tests (8 test cases) +3. Execute integration tests with various parameters +4. Build and test the Docker container +5. Run CDK unit tests + +### What Gets Tested + +- ✓ OpenMP application builds successfully +- ✓ C++ unit tests pass (sequential vs parallel validation) +- ✓ Integration tests with different parameters +- ✓ Docker container builds and runs correctly +- ✓ CDK stack synthesizes without errors + +## The Component Structure + +The whole component contains: + +- An ECR repository for storing the Docker image +- AWS Batch compute environment with managed EC2 instances +- AWS Batch job definition and job queue +- Lambda function for job submission (optional) +- VPC with public and private subnets +- CloudWatch log group for job logs + +## CDK Toolkit + +The [`cdk.json`](./cdk.json) file in the root of this repository includes +instructions for the CDK toolkit on how to execute this program. + +After building your TypeScript code, you will be able to run the CDK toolkit commands as usual: + +```bash + $ cdk ls + + + $ cdk synth + + + $ cdk deploy + + + $ cdk diff + +``` + +## Cleanup + +To completely remove all AWS resources and avoid ongoing charges: + +### Step 1: Cancel Running Jobs + +Before destroying the stack, ensure no jobs are running: + +#### Option A: Using AWS Console (Visual Method) +1. Navigate to [AWS Batch Console](https://console.aws.amazon.com/batch/) +2. Click on "Jobs" in the left sidebar +3. Select your job queue (e.g., "AwsBatchOpenmpBenchmarkStack-OpenMPJobQueue...") +4. Filter by status: RUNNING, RUNNABLE, or PENDING +5. Select any active jobs and click "Terminate job" +6. Provide a reason (e.g., "Stack cleanup") +7. Verify all jobs show as SUCCEEDED, FAILED, or CANCELLED + +#### Option B: Using AWS CLI (Programmatic Method) +```bash +# List running jobs +aws batch list-jobs --profile your-profile \ + --job-queue \ + --job-status RUNNING + +# Terminate each job +aws batch terminate-job --profile your-profile \ + --job-id \ + --reason "Stack cleanup" +``` + +### Step 2: Destroy the Stack + +```bash +npx cdk destroy --profile your-profile +``` + +Type 'y' when prompted to confirm deletion. This will remove: +- ✓ All infrastructure resources (VPC, subnets, NAT gateway) +- ✓ ECR repository and all Docker images inside it +- ✓ Batch compute environment, job queue, and job definitions +- ✓ Lambda function and IAM roles +- ✓ CloudWatch log groups and all logs +- ✓ Security groups and all networking components + +### Step 3: Clean Up Local Artifacts (Optional) + +```bash +# Remove deployment info file +rm -f deployment-info.json + +# Remove all local Docker images (including tagged ECR images) +docker rmi $(docker images | grep openmp-benchmark | awk '{print $3}') + +# Clean up Docker build cache (frees significant disk space) +docker builder prune + +# Clean up any dangling Docker images +docker image prune +``` + +### Verification + +After cleanup, verify in the AWS Console that: +- The CloudFormation stack is deleted +- No ECR repositories remain for this project +- No VPC resources are left behind +- No unexpected charges appear in your AWS billing + +> **Note**: The stack is specifically configured for complete resource deletion. All resources have appropriate removal policies to ensure clean deletion without leaving orphaned resources. diff --git a/typescript/batch-ecr-openmp/bin/aws-batch-openmp-benchmark.ts b/typescript/batch-ecr-openmp/bin/aws-batch-openmp-benchmark.ts new file mode 100644 index 000000000..57ce45c38 --- /dev/null +++ b/typescript/batch-ecr-openmp/bin/aws-batch-openmp-benchmark.ts @@ -0,0 +1,20 @@ +#!/usr/bin/env node +import * as cdk from 'aws-cdk-lib'; +import { AwsBatchOpenmpBenchmarkStack } from '../lib/aws-batch-openmp-benchmark-stack'; + +const app = new cdk.App(); +new AwsBatchOpenmpBenchmarkStack(app, 'AwsBatchOpenmpBenchmarkStack', { + /* 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 line to specialize this stack for the AWS Account + * and Region that are implied by the current CLI configuration. */ + // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, + + /* Uncomment the next line if you know exactly what Account and Region you + * want to deploy the stack to. */ + // env: { account: '123456789012', region: 'us-east-1' }, + + /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ +}); diff --git a/typescript/batch-ecr-openmp/cdk.json b/typescript/batch-ecr-openmp/cdk.json new file mode 100644 index 000000000..b19706556 --- /dev/null +++ b/typescript/batch-ecr-openmp/cdk.json @@ -0,0 +1,97 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/aws-batch-openmp-benchmark.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "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, + "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false, + "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, + "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true, + "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true, + "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true, + "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true, + "@aws-cdk/core:enableAdditionalMetadataCollection": true, + "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": false, + "@aws-cdk/aws-s3:setUniqueReplicationRoleName": true, + "@aws-cdk/aws-events:requireEventBusPolicySid": true, + "@aws-cdk/core:aspectPrioritiesMutating": true, + "@aws-cdk/aws-dynamodb:retainTableReplica": true, + "@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2": true, + "@aws-cdk/s3-notifications:addS3TrustKeyPolicyForSnsSubscriptions": true, + "@aws-cdk/aws-ec2:requirePrivateSubnetsForEgressOnlyInternetGateway": true, + "@aws-cdk/aws-s3:publicAccessBlockedByDefault": true, + "@aws-cdk/aws-lambda:useCdkManagedLogGroup": true + } +} diff --git a/typescript/batch-ecr-openmp/docker/Dockerfile b/typescript/batch-ecr-openmp/docker/Dockerfile new file mode 100644 index 000000000..be221b985 --- /dev/null +++ b/typescript/batch-ecr-openmp/docker/Dockerfile @@ -0,0 +1,51 @@ +# Multi-stage build for OpenMP C++ application +FROM ubuntu:22.04 as builder + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + g++ \ + make \ + libomp-dev \ + && rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /build + +# Copy source code +COPY src/openmp/ . + +# Build the application +RUN make clean && make + +# Runtime stage +FROM ubuntu:22.04 + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + libgomp1 \ + && rm -rf /var/lib/apt/lists/* + +# Create non-root user for security +RUN useradd -m -u 1000 openmp + +# Copy the built executable +COPY --from=builder /build/openmp_benchmark /usr/local/bin/ + +# Set executable permissions +RUN chmod +x /usr/local/bin/openmp_benchmark + +# Switch to non-root user +USER openmp + +# Set working directory +WORKDIR /home/openmp + +# Default command +ENTRYPOINT ["/usr/local/bin/openmp_benchmark"] +CMD ["--json"] + +# Labels for metadata +LABEL maintainer="OpenMP Showcase" +LABEL description="C++20 OpenMP parallel computing benchmark for AWS Batch" +LABEL version="1.0" diff --git a/typescript/batch-ecr-openmp/jest.config.js b/typescript/batch-ecr-openmp/jest.config.js new file mode 100644 index 000000000..08263b895 --- /dev/null +++ b/typescript/batch-ecr-openmp/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: 'node', + roots: ['/test'], + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest' + } +}; diff --git a/typescript/batch-ecr-openmp/lib/aws-batch-openmp-benchmark-stack.ts b/typescript/batch-ecr-openmp/lib/aws-batch-openmp-benchmark-stack.ts new file mode 100644 index 000000000..4bfa0b043 --- /dev/null +++ b/typescript/batch-ecr-openmp/lib/aws-batch-openmp-benchmark-stack.ts @@ -0,0 +1,320 @@ +import * as cdk from 'aws-cdk-lib'; +import * as batch from 'aws-cdk-lib/aws-batch'; +import * as ec2 from 'aws-cdk-lib/aws-ec2'; +import * as ecr from 'aws-cdk-lib/aws-ecr'; +import * as ecs from 'aws-cdk-lib/aws-ecs'; +import * as iam from 'aws-cdk-lib/aws-iam'; + +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as logs from 'aws-cdk-lib/aws-logs'; +import { Construct } from 'constructs'; + +export class AwsBatchOpenmpBenchmarkStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + /* + * COMPLETE DELETION CONFIGURATION + * ================================ + * This stack is configured for complete resource deletion when running 'cdk destroy': + * + * - ECR Repository: emptyOnDelete=true removes all images before deletion + + * - VPC: applyRemovalPolicy(DESTROY) ensures complete VPC cleanup + * - CloudWatch Logs: removalPolicy=DESTROY removes log groups + * - All other resources (IAM roles, security groups, Batch resources, Lambda) + * will be automatically deleted as they have no retention policies + * + * WARNING: Running 'cdk destroy' will permanently delete ALL data and resources! + */ + + + // ECR Repository for OpenMP container + const ecrRepository = new ecr.Repository(this, 'OpenMPRepository', { + repositoryName: 'openmp-benchmark', + removalPolicy: cdk.RemovalPolicy.DESTROY, + emptyOnDelete: true, // Force delete all images on stack deletion + lifecycleRules: [{ + maxImageCount: 10, + description: 'Keep only 10 most recent images' + }] + }); + + + + // VPC for Batch compute environment + const vpc = new ec2.Vpc(this, 'OpenMPVPC', { + maxAzs: 2, + natGateways: 1, + subnetConfiguration: [ + { + cidrMask: 24, + name: 'Public', + subnetType: ec2.SubnetType.PUBLIC, + }, + { + cidrMask: 24, + name: 'Private', + subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, + } + ] + }); + + // Apply removal policy to VPC to ensure complete deletion + vpc.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY); + + // Security Group for Batch compute environment + const batchSecurityGroup = new ec2.SecurityGroup(this, 'BatchSecurityGroup', { + vpc, + description: 'Security group for OpenMP Batch compute environment', + allowAllOutbound: true + }); + + // IAM Role for Batch service + const batchServiceRole = new iam.Role(this, 'BatchServiceRole', { + assumedBy: new iam.ServicePrincipal('batch.amazonaws.com'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSBatchServiceRole') + ] + }); + + // IAM Role for EC2 instances in Batch + const instanceRole = new iam.Role(this, 'BatchInstanceRole', { + assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonEC2ContainerServiceforEC2Role') + ] + }); + + + + // Instance Profile for EC2 instances + const instanceProfile = new iam.CfnInstanceProfile(this, 'BatchInstanceProfile', { + roles: [instanceRole.roleName] + }); + + // Launch Template for compute environment + const launchTemplate = new ec2.LaunchTemplate(this, 'BatchLaunchTemplate', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.C5, ec2.InstanceSize.LARGE), + machineImage: ec2.MachineImage.latestAmazonLinux2({ + edition: ec2.AmazonLinuxEdition.STANDARD + }), + securityGroup: batchSecurityGroup, + userData: ec2.UserData.forLinux(), + role: instanceRole + }); + + // Batch Compute Environment - using cost-effective instance types + const computeEnvironment = new batch.CfnComputeEnvironment(this, 'OpenMPComputeEnvironment', { + type: 'MANAGED', + state: 'ENABLED', + serviceRole: batchServiceRole.roleArn, + computeResources: { + type: 'EC2', + minvCpus: 0, + maxvCpus: 256, + instanceTypes: ['c6i.large', 'c6i.xlarge', 'c6i.2xlarge', 'c5.large', 'c5.xlarge'], + instanceRole: instanceProfile.attrArn, + securityGroupIds: [batchSecurityGroup.securityGroupId], + subnets: vpc.privateSubnets.map(subnet => subnet.subnetId) + } + }); + + // Batch Job Queue + const jobQueue = new batch.CfnJobQueue(this, 'OpenMPJobQueue', { + state: 'ENABLED', + priority: 1, + computeEnvironmentOrder: [ + { + computeEnvironment: computeEnvironment.ref, + order: 1 + } + ] + }); + + // IAM Role for Batch jobs + const jobRole = new iam.Role(this, 'BatchJobRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy') + ] + }); + + + + // CloudWatch Log Group for Batch jobs + const logGroup = new logs.LogGroup(this, 'OpenMPLogGroup', { + logGroupName: '/aws/batch/openmp-benchmark', + removalPolicy: cdk.RemovalPolicy.DESTROY, + retention: logs.RetentionDays.ONE_WEEK + }); + + // Batch Job Definition using CloudFormation with parameter mapping + const jobDefinition = new batch.CfnJobDefinition(this, 'OpenMPJobDefinition', { + type: 'container', + containerProperties: { + image: `${ecrRepository.repositoryUri}:latest`, + vcpus: 2, + memory: 4096, + jobRoleArn: jobRole.roleArn, + executionRoleArn: jobRole.roleArn, + // Map AWS Batch parameters to command line arguments + command: [ + '/usr/local/bin/openmp_benchmark', + '--size', 'Ref::size', + '--threads', 'Ref::threads', + '--json', 'Ref::json', + '--benchmark-type', 'Ref::benchmark-type', + '--matrix-size', 'Ref::matrix-size' + ], + environment: [ + { name: 'AWS_DEFAULT_REGION', value: this.region } + // OMP_NUM_THREADS now auto-detected at runtime for optimal CPU utilization + ], + logConfiguration: { + logDriver: 'awslogs', + options: { + 'awslogs-group': logGroup.logGroupName, + 'awslogs-stream-prefix': 'openmp' + } + } + }, + // Define parameters that can be passed from submit-job (30-min runtime, 4GB memory) + parameters: { + size: '600000000', + threads: '0', + json: 'true', + 'benchmark-type': 'simple', + 'matrix-size': '1200' + } + }); + + // Lambda function for job submission and monitoring + const jobSubmitterFunction = new lambda.Function(this, 'JobSubmitterFunction', { + runtime: lambda.Runtime.PYTHON_3_11, + handler: 'index.handler', + code: lambda.Code.fromInline(` +import json +import boto3 +import uuid +from datetime import datetime + +batch_client = boto3.client('batch') + +def handler(event, context): + try: + # Parse input parameters + problem_size = event.get('problemSize', 100000000) + max_threads = event.get('maxThreads', 0) + instance_type = event.get('instanceType', 't3.large') + + # Map instance types to optimal vCPU counts for OpenMP + instance_vcpu_map = { + 't3.medium': 2, + 't3.large': 2, + 't3.xlarge': 4, + 't3a.medium': 2, + 't3a.large': 2, + 'c6i.large': 2, + 'c6i.xlarge': 4, + 'c6i.2xlarge': 8, + 'c6i.4xlarge': 16, + 'c6i.8xlarge': 32, + 'c5.large': 2, + 'c5.xlarge': 4, + 'c5.2xlarge': 8, + 'c5.4xlarge': 16, + 'c5.9xlarge': 36, + 'c5n.large': 2, + 'c5n.xlarge': 4, + 'c5n.2xlarge': 8 + } + + # Determine optimal thread count + instance_vcpus = instance_vcpu_map.get(instance_type, 4) + optimal_threads = max_threads if max_threads > 0 else instance_vcpus + + # Generate unique job name + job_name = f"openmp-benchmark-{uuid.uuid4().hex[:8]}" + + # Submit batch job with dynamic resource allocation + response = batch_client.submit_job( + jobName=job_name, + jobQueue='${jobQueue.ref}', + jobDefinition='${jobDefinition.ref}', + parameters={ + 'size': str(problem_size), + 'threads': str(optimal_threads), + 'json': 'true' + }, + containerOverrides={ + 'vcpus': instance_vcpus, + 'memory': instance_vcpus * 2048, # 2GB per vCPU + 'environment': [ + {'name': 'AWS_BATCH_JOB_INSTANCE_TYPE', 'value': instance_type}, + {'name': 'OMP_NUM_THREADS', 'value': str(optimal_threads)} + ] + } + ) + + return { + 'statusCode': 200, + 'body': json.dumps({ + 'jobId': response['jobId'], + 'jobName': response['jobName'], + 'message': 'Job submitted successfully' + }) + } + + except Exception as e: + return { + 'statusCode': 500, + 'body': json.dumps({ + 'error': str(e) + }) + } + `), + timeout: cdk.Duration.minutes(5), + environment: { + 'JOB_QUEUE': jobQueue.ref, + 'JOB_DEFINITION': jobDefinition.ref + } + }); + + // Grant Lambda permissions to submit Batch jobs + jobSubmitterFunction.addToRolePolicy(new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + 'batch:SubmitJob', + 'batch:DescribeJobs', + 'batch:ListJobs' + ], + resources: ['*'] + })); + + + + // Outputs + new cdk.CfnOutput(this, 'ECRRepositoryURI', { + value: ecrRepository.repositoryUri, + description: 'ECR Repository URI for OpenMP container' + }); + + new cdk.CfnOutput(this, 'JobQueueName', { + value: jobQueue.ref, + description: 'Batch Job Queue Name' + }); + + new cdk.CfnOutput(this, 'JobDefinitionName', { + value: jobDefinition.ref, + description: 'Batch Job Definition Name' + }); + + + + new cdk.CfnOutput(this, 'LambdaFunctionName', { + value: jobSubmitterFunction.functionName, + description: 'Lambda function for job submission' + }); + } +} diff --git a/typescript/batch-ecr-openmp/package.json b/typescript/batch-ecr-openmp/package.json new file mode 100644 index 000000000..9f388d9ee --- /dev/null +++ b/typescript/batch-ecr-openmp/package.json @@ -0,0 +1,32 @@ +{ + "name": "batch-ecr-lambda", + "version": "1.0.0", + "description": "AWS Batch with containerized application in ECR and Lambda for job submission", + "private": true, + "bin": { + "batch-ecr-lambda": "bin/aws-batch-openmp-benchmark.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "license": "Apache-2.0", + "devDependencies": { + "@types/jest": "^29.5.14", + "@types/node": "22.7.9", + "aws-cdk": "2.1018.1", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "typescript": "~5.6.3" + }, + "dependencies": { + "aws-cdk-lib": "^2.200.1", + "constructs": "^10.4.2" + } +} diff --git a/typescript/batch-ecr-openmp/scripts/build-and-deploy.sh b/typescript/batch-ecr-openmp/scripts/build-and-deploy.sh new file mode 100644 index 000000000..1331c1923 --- /dev/null +++ b/typescript/batch-ecr-openmp/scripts/build-and-deploy.sh @@ -0,0 +1,117 @@ +#!/bin/bash + +# AWS Batch OpenMP Benchmark - Build and Deploy Script +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# AWS Profile (required argument) +if [ -z "$AWS_PROFILE" ]; then + echo -e "${RED}❌ AWS_PROFILE environment variable is required. Please set it before running the script.${NC}" + echo -e "${YELLOW}Example: AWS_PROFILE=my-profile ./scripts/build-and-deploy.sh${NC}" + exit 1 +fi + +echo -e "${GREEN}🚀 AWS Batch OpenMP Benchmark - Build and Deploy${NC}" +echo "==============================================" +echo -e "${YELLOW}Using AWS Profile: ${AWS_PROFILE}${NC}" +echo "" + +# Check if AWS CLI is configured with the profile +if ! aws sts get-caller-identity --profile $AWS_PROFILE > /dev/null 2>&1; then + echo -e "${RED}❌ AWS CLI not configured for profile '$AWS_PROFILE'. Please run 'aws configure --profile $AWS_PROFILE' first.${NC}" + exit 1 +fi + +# Get AWS account and region +AWS_ACCOUNT=$(aws sts get-caller-identity --profile $AWS_PROFILE --query Account --output text) +AWS_REGION=$(aws configure get region --profile $AWS_PROFILE) +ECR_REPOSITORY_URI="${AWS_ACCOUNT}.dkr.ecr.${AWS_REGION}.amazonaws.com/openmp-benchmark" + +echo -e "${YELLOW}📋 Configuration:${NC}" +echo " AWS Profile: $AWS_PROFILE" +echo " AWS Account: $AWS_ACCOUNT" +echo " AWS Region: $AWS_REGION" +echo " ECR Repository: $ECR_REPOSITORY_URI" +echo "" + +# Step 1: Build the CDK stack +echo -e "${YELLOW}🏗️ Step 1: Building CDK stack...${NC}" +npm run build +if [ $? -ne 0 ]; then + echo -e "${RED}❌ CDK build failed${NC}" + exit 1 +fi + +# Step 2: Deploy CDK stack +echo -e "${YELLOW}☁️ Step 2: Deploying CDK stack...${NC}" +npx cdk deploy --profile $AWS_PROFILE --require-approval never +if [ $? -ne 0 ]; then + echo -e "${RED}❌ CDK deployment failed${NC}" + exit 1 +fi + +# Step 3: Get ECR login token and login +echo -e "${YELLOW}🔐 Step 3: Logging into ECR...${NC}" +aws ecr get-login-password --profile $AWS_PROFILE --region $AWS_REGION | docker login --username AWS --password-stdin $ECR_REPOSITORY_URI +if [ $? -ne 0 ]; then + echo -e "${RED}❌ ECR login failed${NC}" + exit 1 +fi + +# Step 4: Build Docker image +echo -e "${YELLOW}🐳 Step 4: Building Docker image...${NC}" +docker build -f docker/Dockerfile -t openmp-benchmark:latest . +if [ $? -ne 0 ]; then + echo -e "${RED}❌ Docker build failed${NC}" + exit 1 +fi + +# Step 5: Tag and push Docker image +echo -e "${YELLOW}📤 Step 5: Pushing Docker image to ECR...${NC}" +docker tag openmp-benchmark:latest $ECR_REPOSITORY_URI:latest +docker push $ECR_REPOSITORY_URI:latest +if [ $? -ne 0 ]; then + echo -e "${RED}❌ Docker push failed${NC}" + exit 1 +fi + +# Step 6: Get stack outputs +echo -e "${YELLOW}📊 Step 6: Getting deployment information...${NC}" +STACK_OUTPUTS=$(aws cloudformation describe-stacks --profile $AWS_PROFILE --stack-name AwsBatchOpenmpBenchmarkStack --query 'Stacks[0].Outputs' --output json) + +# Step 7: Save deployment info for automated job submission +echo -e "${YELLOW}💾 Step 7: Saving deployment information...${NC}" +cat > deployment-info.json << EOF +{ + "awsProfile": "$AWS_PROFILE", + "awsRegion": "$AWS_REGION", + "awsAccount": "$AWS_ACCOUNT", + "stackOutputs": $STACK_OUTPUTS, + "deploymentTimestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)" +} +EOF + +echo -e "${GREEN}✅ Deployment completed successfully!${NC}" +echo -e "${GREEN}✅ Deployment info saved to deployment-info.json${NC}" +echo "" +echo -e "${YELLOW}📋 Deployment Information:${NC}" +echo "$STACK_OUTPUTS" | jq -r '.[] | " \(.OutputKey): \(.OutputValue)"' + +echo "" +echo -e "${GREEN}🎉 Your AWS Batch OpenMP Benchmark is ready!${NC}" +echo "" +echo -e "${YELLOW}Next steps:${NC}" +echo "1. Test the Lambda function to submit jobs" +echo "2. Monitor jobs in AWS Batch console" + +echo "3. View logs in CloudWatch" +echo "" +echo -e "${YELLOW}💡 Usage Tips:${NC}" +echo "• To use a different profile: AWS_PROFILE=my-profile ./scripts/build-and-deploy.sh" +echo "• To test locally first: ./scripts/test-local.sh" +echo "• To clean up: npx cdk destroy --profile $AWS_PROFILE" diff --git a/typescript/batch-ecr-openmp/scripts/submit-job.sh b/typescript/batch-ecr-openmp/scripts/submit-job.sh new file mode 100644 index 000000000..0ccd2ee84 --- /dev/null +++ b/typescript/batch-ecr-openmp/scripts/submit-job.sh @@ -0,0 +1,319 @@ +#!/bin/bash + +# AWS Batch OpenMP Benchmark - Enhanced Job Submission Script +# Supports multiple benchmark types with optimized parameters +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +PURPLE='\033[0;35m' +NC='\033[0m' # No Color + +# Enhanced default parameters for quick benchmarks (4GB memory limit) +DEFAULT_SIZE=600000000 # 600M elements for ~5 second runtime (4GB memory) +DEFAULT_MATRIX_SIZE=1200 # 1200x1200 matrix for ~3 second target +DEFAULT_THREADS=0 # 0 = auto-detect all available cores +DEFAULT_BENCHMARK="simple" # Default benchmark type +DEFAULT_INSTANCE="c5.large" # Cost-optimized default for demo +DEFAULT_METHOD="batch" +DRY_RUN=false + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + --size) + PROBLEM_SIZE="$2" + shift 2 + ;; + --matrix-size) + MATRIX_SIZE="$2" + shift 2 + ;; + --threads) + THREAD_COUNT="$2" + shift 2 + ;; + --benchmark-type) + BENCHMARK_TYPE="$2" + shift 2 + ;; + --instance) + INSTANCE_TYPE="$2" + shift 2 + ;; + --method) + METHOD="$2" + shift 2 + ;; + --dry-run) + DRY_RUN=true + shift + ;; + --help|-h) + echo -e "${GREEN}🚀 AWS Batch OpenMP Enhanced Benchmark Suite${NC}" + echo "=============================================" + echo "" + echo -e "${YELLOW}Usage:${NC}" + echo " $0 [OPTIONS]" + echo "" + echo -e "${YELLOW}Options:${NC}" + echo " --size SIZE Problem size for simple/math/heavy benchmarks (default: $DEFAULT_SIZE)" + echo " --matrix-size SIZE Matrix size for matrix benchmark (default: $DEFAULT_MATRIX_SIZE)" + echo " --threads COUNT Thread count, 0=auto-detect all cores (default: $DEFAULT_THREADS)" + echo " --benchmark-type TYPE Benchmark type (default: $DEFAULT_BENCHMARK)" + echo " --instance TYPE Instance type (default: $DEFAULT_INSTANCE)" + echo " --method METHOD Submission method: batch|lambda (default: $DEFAULT_METHOD)" + echo " --dry-run Show commands without executing" + echo " --help, -h Show this help message" + echo "" + echo -e "${YELLOW}Benchmark Types:${NC}" + echo " ${PURPLE}simple${NC} - Basic arithmetic operations (fastest, ~2 seconds)" + echo " ${PURPLE}math${NC} - Mathematical functions: sin, cos, sqrt, pow (~5 seconds)" + echo " ${PURPLE}matrix${NC} - Matrix multiplication (memory intensive, ~3 seconds)" + echo " ${PURPLE}heavy${NC} - Complex arithmetic expressions (~3 seconds)" + echo " ${PURPLE}all${NC} - Run all benchmarks sequentially (~15 seconds total)" + echo "" + echo -e "${YELLOW}Instance Types (Recommended):${NC}" + echo " ${BLUE}c6i.large${NC} - 2 vCPUs, 4 GB RAM (development/testing)" + echo " ${BLUE}c6i.xlarge${NC} - 4 vCPUs, 8 GB RAM (recommended default)" + echo " ${BLUE}c6i.2xlarge${NC} - 8 vCPUs, 16 GB RAM (maximum performance)" + echo "" + echo -e "${YELLOW}Examples:${NC}" + echo " $0 # Simple benchmark with defaults" + echo " $0 --benchmark-type all # Run comprehensive 5-minute suite" + echo " $0 --benchmark-type matrix --instance c6i.2xlarge # Matrix benchmark on 8 cores" + echo " $0 --benchmark-type math --size 100000000 # Math benchmark with custom size" + echo " $0 --method lambda --benchmark-type heavy # Heavy benchmark via Lambda" + echo " $0 --dry-run --benchmark-type all # Preview all benchmark commands" + echo "" + echo -e "${YELLOW}Expected Runtimes:${NC}" + echo " Simple: ~2 seconds (basic arithmetic)" + echo " Math: ~5 seconds (floating point intensive)" + echo " Matrix: ~3 seconds (memory bound)" + echo " Heavy: ~3 seconds (complex expressions)" + echo " All: ~15 seconds total (runs all benchmarks)" + exit 0 + ;; + *) + echo -e "${RED}❌ Unknown option: $1${NC}" + echo "Use --help for usage information" + exit 1 + ;; + esac +done + +# Set defaults for unspecified parameters +PROBLEM_SIZE=${PROBLEM_SIZE:-$DEFAULT_SIZE} +MATRIX_SIZE=${MATRIX_SIZE:-$DEFAULT_MATRIX_SIZE} +THREAD_COUNT=${THREAD_COUNT:-$DEFAULT_THREADS} +BENCHMARK_TYPE=${BENCHMARK_TYPE:-$DEFAULT_BENCHMARK} +INSTANCE_TYPE=${INSTANCE_TYPE:-$DEFAULT_INSTANCE} +METHOD=${METHOD:-$DEFAULT_METHOD} + +# Validate benchmark type +case $BENCHMARK_TYPE in + simple|math|matrix|heavy|all) + # Valid benchmark type + ;; + *) + echo -e "${RED}❌ Invalid benchmark type: $BENCHMARK_TYPE${NC}" + echo -e "${YELLOW}Valid types: simple, math, matrix, heavy, all${NC}" + exit 1 + ;; +esac + +echo -e "${GREEN}🚀 AWS Batch OpenMP Enhanced Benchmark Suite${NC}" +echo "=============================================" + +# Check if deployment-info.json exists +if [ ! -f "deployment-info.json" ]; then + echo -e "${RED}❌ deployment-info.json not found!${NC}" + echo "" + echo -e "${YELLOW}Please run the deployment script first:${NC}" + echo " ./scripts/build-and-deploy.sh" + echo "" + echo "This will create the deployment-info.json file with your AWS resource information." + exit 1 +fi + +# Read deployment information +echo -e "${YELLOW}📋 Reading deployment information...${NC}" +if ! command -v jq &> /dev/null; then + echo -e "${RED}❌ jq is required but not installed. Please install jq first.${NC}" + echo "" + echo -e "${YELLOW}Installation:${NC}" + echo " Ubuntu/Debian: sudo apt-get install jq" + echo " macOS: brew install jq" + echo " Amazon Linux: sudo yum install jq" + exit 1 +fi + +# Extract information from deployment-info.json +AWS_PROFILE=$(jq -r '.awsProfile' deployment-info.json) +AWS_REGION=$(jq -r '.awsRegion' deployment-info.json) +AWS_ACCOUNT=$(jq -r '.awsAccount' deployment-info.json) + +# Extract stack outputs +JOB_QUEUE_NAME=$(jq -r '.stackOutputs[] | select(.OutputKey=="JobQueueName") | .OutputValue' deployment-info.json) +JOB_DEFINITION_NAME=$(jq -r '.stackOutputs[] | select(.OutputKey=="JobDefinitionName") | .OutputValue' deployment-info.json) +LAMBDA_FUNCTION_NAME=$(jq -r '.stackOutputs[] | select(.OutputKey=="LambdaFunctionName") | .OutputValue' deployment-info.json) + + +# Validate that we have the required information +if [ "$JOB_QUEUE_NAME" = "null" ] || [ "$JOB_DEFINITION_NAME" = "null" ] || [ -z "$JOB_QUEUE_NAME" ] || [ -z "$JOB_DEFINITION_NAME" ]; then + echo -e "${RED}❌ Could not extract AWS Batch information from deployment-info.json${NC}" + echo "" + echo -e "${YELLOW}The deployment may have failed or the file is corrupted.${NC}" + echo "Please redeploy using: ./scripts/build-and-deploy.sh" + exit 1 +fi + +# Display estimated runtime based on benchmark type +case $BENCHMARK_TYPE in + simple) + ESTIMATED_TIME="~2 seconds" + ;; + math) + ESTIMATED_TIME="~5 seconds" + ;; + matrix) + ESTIMATED_TIME="~3 seconds" + ;; + heavy) + ESTIMATED_TIME="~3 seconds" + ;; + all) + ESTIMATED_TIME="~15 seconds total" + ;; +esac + +echo -e "${YELLOW}Configuration:${NC}" +echo " AWS Profile: $AWS_PROFILE" +echo " AWS Region: $AWS_REGION" +echo " Job Queue: $JOB_QUEUE_NAME" +echo " Job Definition: $JOB_DEFINITION_NAME" +echo " Benchmark Type: ${PURPLE}$BENCHMARK_TYPE${NC} (estimated: $ESTIMATED_TIME)" +echo " Problem Size: $PROBLEM_SIZE" +if [ "$BENCHMARK_TYPE" = "matrix" ]; then + echo " Matrix Size: ${MATRIX_SIZE}x${MATRIX_SIZE}" +fi +echo " Thread Count: $THREAD_COUNT $([ "$THREAD_COUNT" = "0" ] && echo "(auto-detect all cores)")" +echo " Instance Type: $INSTANCE_TYPE" +echo " Method: $METHOD" +echo "" + +# Generate unique job name with benchmark type +JOB_NAME="openmp-${BENCHMARK_TYPE}-$(date +%s)" + +# Build parameters based on benchmark type +PARAMS="size=$PROBLEM_SIZE,threads=$THREAD_COUNT,json=true" +if [ "$BENCHMARK_TYPE" != "simple" ]; then + PARAMS="$PARAMS,benchmark-type=$BENCHMARK_TYPE" +fi +if [ "$BENCHMARK_TYPE" = "matrix" ]; then + PARAMS="$PARAMS,matrix-size=$MATRIX_SIZE" +fi + +# Submit job based on method +case $METHOD in + "batch") + echo -e "${YELLOW}🚀 Submitting $BENCHMARK_TYPE benchmark job via AWS Batch...${NC}" + + BATCH_COMMAND="aws batch submit-job --profile $AWS_PROFILE \\ + --job-name \"$JOB_NAME\" \\ + --job-queue \"$JOB_QUEUE_NAME\" \\ + --job-definition \"$JOB_DEFINITION_NAME\" \\ + --parameters $PARAMS" + + if [ "$DRY_RUN" = true ]; then + echo -e "${BLUE}Dry run - would execute:${NC}" + echo "$BATCH_COMMAND" + else + echo -e "${YELLOW}Executing:${NC} $BATCH_COMMAND" + RESULT=$(eval "$BATCH_COMMAND") + if [ $? -eq 0 ]; then + JOB_ID=$(echo "$RESULT" | jq -r '.jobId') + echo "" + echo -e "${GREEN}✅ Job submitted successfully!${NC}" + echo "Job Name: $JOB_NAME" + echo "Job ID: $JOB_ID" + echo "Benchmark: ${PURPLE}$BENCHMARK_TYPE${NC}" + echo "Estimated Runtime: $ESTIMATED_TIME" + else + echo -e "${RED}❌ Job submission failed${NC}" + exit 1 + fi + fi + ;; + + "lambda") + echo -e "${YELLOW}🚀 Submitting $BENCHMARK_TYPE benchmark job via Lambda function...${NC}" + + if [ "$LAMBDA_FUNCTION_NAME" = "null" ] || [ -z "$LAMBDA_FUNCTION_NAME" ]; then + echo -e "${RED}❌ Lambda function name not found in deployment info${NC}" + exit 1 + fi + + LAMBDA_PAYLOAD="{\"problemSize\": $PROBLEM_SIZE, \"maxThreads\": $THREAD_COUNT, \"instanceType\": \"$INSTANCE_TYPE\", \"benchmarkType\": \"$BENCHMARK_TYPE\"" + if [ "$BENCHMARK_TYPE" = "matrix" ]; then + LAMBDA_PAYLOAD="$LAMBDA_PAYLOAD, \"matrixSize\": $MATRIX_SIZE" + fi + LAMBDA_PAYLOAD="$LAMBDA_PAYLOAD}" + + LAMBDA_COMMAND="aws lambda invoke --profile $AWS_PROFILE \\ + --function-name \"$LAMBDA_FUNCTION_NAME\" \\ + --cli-binary-format raw-in-base64-out \\ + --payload '$LAMBDA_PAYLOAD' \\ + response.json" + + if [ "$DRY_RUN" = true ]; then + echo -e "${BLUE}Dry run - would execute:${NC}" + echo "$LAMBDA_COMMAND" + echo "cat response.json" + else + echo -e "${YELLOW}Executing:${NC} $LAMBDA_COMMAND" + eval "$LAMBDA_COMMAND" + if [ $? -eq 0 ]; then + echo "" + echo -e "${GREEN}✅ Lambda invocation successful!${NC}" + echo -e "${YELLOW}Response:${NC}" + cat response.json | jq '.' + rm -f response.json + else + echo -e "${RED}❌ Lambda invocation failed${NC}" + exit 1 + fi + fi + ;; + + *) + echo -e "${RED}❌ Unknown method: $METHOD${NC}" + echo "Supported methods: batch, lambda" + exit 1 + ;; +esac + +echo "" +echo -e "${YELLOW}📊 Monitoring & Next Steps:${NC}" +echo "1. Monitor job status:" +echo " aws batch describe-jobs --profile $AWS_PROFILE --jobs " +echo "" +echo "2. View real-time job logs (once running):" +echo " aws logs describe-log-streams --profile $AWS_PROFILE --log-group-name /aws/batch/openmp-benchmark --order-by LastEventTime --descending" +echo "" + +echo "3. AWS Console URLs:" +echo " Batch: https://$AWS_REGION.console.aws.amazon.com/batch/home#queues" +echo " CloudWatch: https://$AWS_REGION.console.aws.amazon.com/cloudwatch/home#logsV2:log-groups/log-group//aws/batch/openmp-benchmark" +echo "" +echo -e "${PURPLE}💡 Pro Tips:${NC}" +if [ "$BENCHMARK_TYPE" = "all" ]; then + echo "• The 'all' benchmark runs 4 benchmarks sequentially - expect ~15 seconds total" + echo "• Watch CPU utilization in CloudWatch - should see sustained 90%+ usage" +fi +echo "• Thread count 0 uses auto-detection for optimal CPU core utilization" +echo "• Larger instance types (c6i.2xlarge) will show better parallel scaling" +echo "• Check CloudWatch logs for detailed performance metrics and GFLOPS" diff --git a/typescript/batch-ecr-openmp/scripts/test-local.sh b/typescript/batch-ecr-openmp/scripts/test-local.sh new file mode 100644 index 000000000..96e0a888e --- /dev/null +++ b/typescript/batch-ecr-openmp/scripts/test-local.sh @@ -0,0 +1,117 @@ +#!/bin/bash + +# AWS Batch OpenMP Benchmark - Local Testing Script +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${GREEN}🧪 AWS Batch OpenMP Benchmark - Local Testing${NC}" +echo "========================" + +# Check if Docker is running +if ! docker info > /dev/null 2>&1; then + echo -e "${RED}❌ Docker is not running. Please start Docker first.${NC}" + exit 1 +fi + +# Step 1: Build the OpenMP application locally +echo -e "${YELLOW}🔨 Step 1: Building OpenMP application...${NC}" +cd src/openmp +make clean && make +if [ $? -ne 0 ]; then + echo -e "${RED}❌ Local build failed${NC}" + exit 1 +fi + +# Step 2: Run Unit Tests +echo -e "${YELLOW}🧪 Step 2: Running C++ Unit Tests...${NC}" +make unit-test +if [ $? -ne 0 ]; then + echo -e "${RED}❌ Unit tests failed${NC}" + exit 1 +fi + +# Step 3: Test the application locally +echo "" +echo -e "${YELLOW}🚀 Step 3: Testing OpenMP application locally...${NC}" +echo "" +echo -e "${YELLOW}Integration Test 1: Default settings (Sequential vs Parallel Comparison)${NC}" +./openmp_benchmark + +echo "" +echo -e "${YELLOW}Integration Test 2: JSON output${NC}" +./openmp_benchmark --json + +echo "" +echo -e "${YELLOW}Integration Test 3: Sequential only${NC}" +./openmp_benchmark --sequential-only --size 10000000 + +echo "" +echo -e "${YELLOW}Integration Test 4: Parallel only${NC}" +./openmp_benchmark --parallel-only --size 10000000 + +echo "" +echo -e "${YELLOW}Integration Test 5: Custom parameters${NC}" +./openmp_benchmark --size 10000000 --threads 2 + +echo "" +echo -e "${YELLOW}Integration Test 6: Help message${NC}" +./openmp_benchmark --help + +cd ../.. + +# Step 4: Build Docker image locally +echo "" +echo -e "${YELLOW}🐳 Step 4: Building Docker image locally...${NC}" +docker build -f docker/Dockerfile -t openmp-benchmark:test . +if [ $? -ne 0 ]; then + echo -e "${RED}❌ Docker build failed${NC}" + exit 1 +fi + +# Step 5: Test Docker container +echo -e "${YELLOW}🧪 Step 5: Testing Docker container...${NC}" +echo "" +echo -e "${YELLOW}Container Test 1: Default run${NC}" +docker run --rm openmp-benchmark:test + +echo "" +echo -e "${YELLOW}Container Test 2: Custom parameters${NC}" +docker run --rm openmp-benchmark:test --size 5000000 --threads 1 + +echo "" +echo -e "${YELLOW}Container Test 3: Help${NC}" +docker run --rm openmp-benchmark:test --help + +# Step 6: CDK Unit Tests +echo "" +echo -e "${YELLOW}🧪 Step 6: Running CDK unit tests...${NC}" +npm test +if [ $? -ne 0 ]; then + echo -e "${RED}❌ CDK unit tests failed${NC}" + exit 1 +fi + +echo "" +echo -e "${GREEN}✅ All local tests passed!${NC}" +echo "" +echo -e "${YELLOW}📋 Summary:${NC}" +echo " ✓ OpenMP application builds successfully" +echo " ✓ C++ unit tests pass (8/8 tests)" +echo " ✓ Sequential and parallel implementations validated" +echo " ✓ Integration tests pass (6 test scenarios)" +echo " ✓ Docker container builds and runs correctly" +echo " ✓ CDK unit tests pass" +echo " ✓ CDK stack synthesizes without errors" +echo "" +echo -e "${GREEN}🎉 Ready for deployment!${NC}" +echo "" +echo -e "${YELLOW}Next steps:${NC}" +echo "1. Run 'AWS_PROFILE=my-profile ./scripts/build-and-deploy.sh' to deploy to AWS" +echo "2. Or run individual components:" +echo " - 'npx cdk deploy' for infrastructure only" +echo " - 'docker build' and 'docker push' for container only" diff --git a/typescript/batch-ecr-openmp/src/openmp/Makefile b/typescript/batch-ecr-openmp/src/openmp/Makefile new file mode 100644 index 000000000..4b6f193f7 --- /dev/null +++ b/typescript/batch-ecr-openmp/src/openmp/Makefile @@ -0,0 +1,46 @@ +CXX = g++ +CXXFLAGS = -std=c++20 -O3 -fopenmp -Wall -Wextra +TARGET = openmp_benchmark +TEST_TARGET = test_benchmark +SOURCE = main.cpp +TEST_SOURCE = test_benchmark.cpp + +# Default target +all: $(TARGET) + +# Build the executable +$(TARGET): $(SOURCE) + $(CXX) $(CXXFLAGS) -o $(TARGET) $(SOURCE) + +# Build the unit test executable +$(TEST_TARGET): $(TEST_SOURCE) + $(CXX) $(CXXFLAGS) -o $(TEST_TARGET) $(TEST_SOURCE) + +# Clean build artifacts +clean: + rm -f $(TARGET) $(TEST_TARGET) *.o + +# Run unit tests +unit-test: $(TEST_TARGET) + @echo "Running unit tests..." + ./$(TEST_TARGET) + +# Test with different configurations +integration-test: $(TARGET) + @echo "Running integration tests..." + @echo "Testing with default settings:" + ./$(TARGET) + @echo "\nTesting with JSON output:" + ./$(TARGET) --json + @echo "\nTesting with custom size and threads:" + ./$(TARGET) --size 50000000 --threads 4 + +# Run all tests +test: unit-test integration-test + @echo "\nAll tests completed!" + +# Install (for Docker) +install: $(TARGET) + cp $(TARGET) /usr/local/bin/ + +.PHONY: all clean test unit-test integration-test install diff --git a/typescript/batch-ecr-openmp/src/openmp/main.cpp b/typescript/batch-ecr-openmp/src/openmp/main.cpp new file mode 100644 index 000000000..fd5baa4c9 --- /dev/null +++ b/typescript/batch-ecr-openmp/src/openmp/main.cpp @@ -0,0 +1,534 @@ +#include // For std::cout +#include // For std::vector +#include // For std::string and C++20 features like starts_with +#include // For timing +#include // For OpenMP directives +#include // For atoi, getenv +#include // For JSON formatting +#include // For mathematical functions +#include // For std::fill +#include // For random number generation + +enum class BenchmarkType { + SIMPLE, + MATH, + MATRIX, + HEAVY, + ALL +}; + +struct BenchmarkResult { + long long sum; + double execution_time; + int thread_count; + int problem_size; + std::string instance_type; + std::string version_type; // "sequential" or "parallel" + std::string benchmark_name; + double flops; // Floating point operations performed +}; + +struct MultiBenchmarkResult { + std::vector sequential_results; + std::vector parallel_results; + double total_time; + int total_threads; + std::string instance_type; +}; + +struct ComparisonResult { + BenchmarkResult sequential; + BenchmarkResult parallel; + double speedup; + double efficiency; +}; + +void printJsonComparison(const ComparisonResult& comparison) { + auto now = std::chrono::system_clock::now(); + auto time_t = std::chrono::system_clock::to_time_t(now); + + std::cout << "{\n"; + std::cout << " \"benchmark_type\": \"sequential_vs_parallel_comparison\",\n"; + std::cout << " \"benchmark_name\": \"" << comparison.parallel.benchmark_name << "\",\n"; + std::cout << " \"problem_size\": " << comparison.sequential.problem_size << ",\n"; + std::cout << " \"sequential\": {\n"; + std::cout << " \"calculated_sum\": " << comparison.sequential.sum << ",\n"; + std::cout << " \"execution_time_seconds\": " << std::fixed << std::setprecision(6) << comparison.sequential.execution_time << ",\n"; + std::cout << " \"gflops\": " << std::fixed << std::setprecision(2) << (comparison.sequential.flops / comparison.sequential.execution_time / 1000000000.0) << ",\n"; + std::cout << " \"thread_count\": " << comparison.sequential.thread_count << "\n"; + std::cout << " },\n"; + std::cout << " \"parallel\": {\n"; + std::cout << " \"calculated_sum\": " << comparison.parallel.sum << ",\n"; + std::cout << " \"execution_time_seconds\": " << std::fixed << std::setprecision(6) << comparison.parallel.execution_time << ",\n"; + std::cout << " \"gflops\": " << std::fixed << std::setprecision(2) << (comparison.parallel.flops / comparison.parallel.execution_time / 1000000000.0) << ",\n"; + std::cout << " \"thread_count\": " << comparison.parallel.thread_count << "\n"; + std::cout << " },\n"; + std::cout << " \"comparison\": {\n"; + std::cout << " \"speedup\": " << std::fixed << std::setprecision(2) << comparison.speedup << ",\n"; + std::cout << " \"efficiency_percent\": " << std::fixed << std::setprecision(2) << comparison.efficiency << ",\n"; + std::cout << " \"results_match\": " << (comparison.sequential.sum == comparison.parallel.sum ? "true" : "false") << "\n"; + std::cout << " },\n"; + std::cout << " \"performance_unit\": \"gflops\",\n"; + std::cout << " \"instance_type\": \"" << comparison.sequential.instance_type << "\",\n"; + std::cout << " \"timestamp\": \"" << std::put_time(std::gmtime(&time_t), "%Y-%m-%dT%H:%M:%SZ") << "\",\n"; +#ifdef _OPENMP + std::cout << " \"openmp_version\": \"" << _OPENMP << "\",\n"; +#else + std::cout << " \"openmp_version\": \"not_available\",\n"; +#endif + std::cout << " \"cpp_standard\": \"C++20\"\n"; + std::cout << "}\n"; +} + +// Simple arithmetic benchmark +BenchmarkResult runSimpleBenchmark(bool parallel, int problem_size, const std::string& instance_type) { + std::vector data(problem_size); + long long sum_result = 0; + + auto start_time = std::chrono::high_resolution_clock::now(); + + if (parallel) { + #pragma omp parallel for reduction(+:sum_result) + for (int i = 0; i < problem_size; ++i) { + data[i] = i * 2; // Simple computation + sum_result += data[i]; + } + } else { + for (int i = 0; i < problem_size; ++i) { + data[i] = i * 2; // Simple computation + sum_result += data[i]; + } + } + + auto end_time = std::chrono::high_resolution_clock::now(); + std::chrono::duration duration = end_time - start_time; + + BenchmarkResult result; + result.sum = sum_result; + result.execution_time = duration.count(); + result.thread_count = parallel ? omp_get_max_threads() : 1; + result.problem_size = problem_size; + result.instance_type = instance_type; + result.version_type = parallel ? "parallel" : "sequential"; + result.benchmark_name = "simple"; + result.flops = problem_size * 2.0; // 2 ops per element: multiply + add + + return result; +} + +// Math Functions Benchmark - CPU intensive floating point operations +BenchmarkResult runMathBenchmark(bool parallel, int problem_size, const std::string& instance_type) { + std::vector data(problem_size); + double sum_result = 0.0; + + auto start_time = std::chrono::high_resolution_clock::now(); + + if (parallel) { + #pragma omp parallel for reduction(+:sum_result) + for (int i = 0; i < problem_size; ++i) { + double x = i * 0.001; // Scale to avoid overflow + // CPU-intensive mathematical operations + double result = std::sin(x) + std::cos(x * 1.5) + std::sqrt(i + 1.0) + std::pow(x, 2.3); + data[i] = result; + sum_result += result; + } + } else { + for (int i = 0; i < problem_size; ++i) { + double x = i * 0.001; + double result = std::sin(x) + std::cos(x * 1.5) + std::sqrt(i + 1.0) + std::pow(x, 2.3); + data[i] = result; + sum_result += result; + } + } + + auto end_time = std::chrono::high_resolution_clock::now(); + std::chrono::duration duration = end_time - start_time; + + BenchmarkResult result; + result.sum = static_cast(sum_result * 1000); // Scale for integer representation + result.execution_time = duration.count(); + result.thread_count = parallel ? omp_get_max_threads() : 1; + result.problem_size = problem_size; + result.instance_type = instance_type; + result.version_type = parallel ? "parallel" : "sequential"; + result.benchmark_name = "math"; + result.flops = problem_size * 8.0; // sin + cos + sqrt + pow + 3 additions + 1 multiply + + return result; +} + +// Matrix Multiplication Benchmark - Memory intensive operations +BenchmarkResult runMatrixBenchmark(bool parallel, int matrix_size, const std::string& instance_type) { + std::vector> A(matrix_size, std::vector(matrix_size)); + std::vector> B(matrix_size, std::vector(matrix_size)); + std::vector> C(matrix_size, std::vector(matrix_size, 0.0)); + + // Initialize matrices with simple values + for (int i = 0; i < matrix_size; ++i) { + for (int j = 0; j < matrix_size; ++j) { + A[i][j] = (i + j) * 0.1; + B[i][j] = (i - j + 1) * 0.1; + } + } + + auto start_time = std::chrono::high_resolution_clock::now(); + + if (parallel) { + #pragma omp parallel for collapse(2) + for (int i = 0; i < matrix_size; ++i) { + for (int j = 0; j < matrix_size; ++j) { + for (int k = 0; k < matrix_size; ++k) { + C[i][j] += A[i][k] * B[k][j]; + } + } + } + } else { + for (int i = 0; i < matrix_size; ++i) { + for (int j = 0; j < matrix_size; ++j) { + for (int k = 0; k < matrix_size; ++k) { + C[i][j] += A[i][k] * B[k][j]; + } + } + } + } + + auto end_time = std::chrono::high_resolution_clock::now(); + std::chrono::duration duration = end_time - start_time; + + // Calculate checksum + double sum_result = 0.0; + for (int i = 0; i < matrix_size; ++i) { + for (int j = 0; j < matrix_size; ++j) { + sum_result += C[i][j]; + } + } + + BenchmarkResult result; + result.sum = static_cast(sum_result * 100); // Scale for integer representation + result.execution_time = duration.count(); + result.thread_count = parallel ? omp_get_max_threads() : 1; + result.problem_size = matrix_size * matrix_size; // Total elements + result.instance_type = instance_type; + result.version_type = parallel ? "parallel" : "sequential"; + result.benchmark_name = "matrix"; + result.flops = static_cast(matrix_size) * matrix_size * matrix_size * 2.0; // N^3 * 2 (multiply + add) + + return result; +} + +// Heavy Arithmetic Benchmark - Complex mathematical expressions +BenchmarkResult runHeavyArithmeticBenchmark(bool parallel, int problem_size, const std::string& instance_type) { + std::vector data(problem_size); + double sum_result = 0.0; + + auto start_time = std::chrono::high_resolution_clock::now(); + + if (parallel) { + #pragma omp parallel for reduction(+:sum_result) + for (int i = 0; i < problem_size; ++i) { + double x = i + 1.0; + // Complex arithmetic expression with multiple operations + double result = (x * 3.14159) + std::sqrt(x) - std::pow(x, 0.3) + + std::log(x) + std::exp(x * 0.0001) + std::sin(x * 0.01) * std::cos(x * 0.02); + data[i] = result; + sum_result += result; + } + } else { + for (int i = 0; i < problem_size; ++i) { + double x = i + 1.0; + double result = (x * 3.14159) + std::sqrt(x) - std::pow(x, 0.3) + + std::log(x) + std::exp(x * 0.0001) + std::sin(x * 0.01) * std::cos(x * 0.02); + data[i] = result; + sum_result += result; + } + } + + auto end_time = std::chrono::high_resolution_clock::now(); + std::chrono::duration duration = end_time - start_time; + + BenchmarkResult result; + result.sum = static_cast(sum_result * 1000); // Scale for integer representation + result.execution_time = duration.count(); + result.thread_count = parallel ? omp_get_max_threads() : 1; + result.problem_size = problem_size; + result.instance_type = instance_type; + result.version_type = parallel ? "parallel" : "sequential"; + result.benchmark_name = "heavy"; + result.flops = problem_size * 12.0; // Multiple math operations per element + + return result; +} + +int main(int argc, char* argv[]) { + // Parse command line arguments + int problem_size = 600000000; // Default 600M for seconds-level runtime (4GB memory limit) + int matrix_size = 1800; // Default matrix size for seconds-level target + int max_threads = 0; // 0 means use all available (auto-detect) + bool json_output = false; + BenchmarkType benchmark_type = BenchmarkType::SIMPLE; + + for (int i = 1; i < argc; i++) { + std::string arg = argv[i]; + if (arg == "--size" && i + 1 < argc) { + problem_size = std::atoi(argv[++i]); + } else if (arg == "--matrix-size" && i + 1 < argc) { + matrix_size = std::atoi(argv[++i]); + } else if (arg == "--threads" && i + 1 < argc) { + max_threads = std::atoi(argv[++i]); + } else if (arg == "--benchmark-type" && i + 1 < argc) { + std::string type = argv[++i]; + if (type == "simple") benchmark_type = BenchmarkType::SIMPLE; + else if (type == "math") benchmark_type = BenchmarkType::MATH; + else if (type == "matrix") benchmark_type = BenchmarkType::MATRIX; + else if (type == "heavy") benchmark_type = BenchmarkType::HEAVY; + else if (type == "all") benchmark_type = BenchmarkType::ALL; + else { + std::cerr << "Invalid benchmark type: " << type << "\n"; + return 1; + } + } else if (arg == "--json") { + json_output = true; + } else if (arg == "--help") { + std::cout << "Usage: " << argv[0] << " [options]\n"; + std::cout << "Options:\n"; + std::cout << " --size N Problem size (default: 750000000)\n"; + std::cout << " --matrix-size N Matrix size for matrix benchmark (default: 1200)\n"; + std::cout << " --threads N Max threads to use (default: auto-detect all)\n"; + std::cout << " --benchmark-type TYPE Benchmark type: simple|math|matrix|heavy|all (default: simple)\n"; + std::cout << " --json Output results in JSON format\n"; + std::cout << " --help Show this help message\n"; + std::cout << "\nBenchmark Types:\n"; + std::cout << " simple - Basic arithmetic operations (fastest)\n"; + std::cout << " math - Mathematical functions (sin, cos, sqrt, pow)\n"; + std::cout << " matrix - Matrix multiplication (memory intensive)\n"; + std::cout << " heavy - Complex arithmetic expressions\n"; + std::cout << " all - Run all benchmarks sequentially (~few seconds)\n"; + return 0; + } + } + + // Auto-detect thread count if not specified + if (max_threads == 0) { + max_threads = omp_get_max_threads(); + } + + // Set thread count + omp_set_num_threads(max_threads); + + // Get instance type from environment (set by AWS Batch) or detect local system + std::string instance_type = "local"; + const char* instance_env = std::getenv("AWS_BATCH_JOB_INSTANCE_TYPE"); + if (instance_env) { + instance_type = instance_env; + } else { + const char* hostname_env = std::getenv("HOSTNAME"); + if (hostname_env) { + std::string hostname = hostname_env; + if (hostname.find(".compute.internal") != std::string::npos) { + instance_type = "aws-development"; + } else { + instance_type = "local-" + hostname; + } + } else { + instance_type = "local-development"; + } + } + + if (!json_output) { + std::cout << "C++20 OpenMP Enhanced Benchmark Suite\n"; + std::cout << "====================================\n"; + + // --- Demonstrate C++20 feature: std::string::starts_with --- + std::string greeting = "Hello, C++20!"; + if (greeting.starts_with("Hello")) { + std::cout << "✓ C++20 string::starts_with works!\n"; + } + + std::cout << "Auto-detected " << max_threads << " CPU cores for optimal threading\n"; + std::cout << "Instance type: " << instance_type << "\n\n"; + } + + // Calibrated problem sizes for seconds-level "all" benchmark target + int simple_size = problem_size; // Use provided size or default 4.5B + int math_size = 100000000; // 100M elements for math functions (4GB memory) + int heavy_size = 50000000; // 50M elements for heavy arithmetic (4GB memory) + + // Run benchmarks based on type + if (benchmark_type == BenchmarkType::ALL) { + // Run all benchmarks for comprehensive comparison (seconds-level total) + std::vector seq_results, par_results; + + auto total_start = std::chrono::high_resolution_clock::now(); + + if (!json_output) { + std::cout << "Running comprehensive benchmark suite (estimated few seconds)...\n"; + std::cout << "===========================================================\n\n"; + } + + // 1. Simple Benchmark (1-1.5 minutes) + if (!json_output) std::cout << "[1/4] Running Simple Benchmark (" << simple_size << " elements)...\n"; + seq_results.push_back(runSimpleBenchmark(false, simple_size, instance_type)); + par_results.push_back(runSimpleBenchmark(true, simple_size, instance_type)); + + // 2. Math Functions Benchmark (1.5-2 minutes) + if (!json_output) std::cout << "[2/4] Running Math Functions Benchmark (" << math_size << " elements)...\n"; + seq_results.push_back(runMathBenchmark(false, math_size, instance_type)); + par_results.push_back(runMathBenchmark(true, math_size, instance_type)); + + // 3. Matrix Multiplication Benchmark (2-2.5 minutes) + if (!json_output) std::cout << "[3/4] Running Matrix Multiplication Benchmark (" << matrix_size << "x" << matrix_size << ")...\n"; + seq_results.push_back(runMatrixBenchmark(false, matrix_size, instance_type)); + par_results.push_back(runMatrixBenchmark(true, matrix_size, instance_type)); + + // 4. Heavy Arithmetic Benchmark (1-1.5 minutes) + if (!json_output) std::cout << "[4/4] Running Heavy Arithmetic Benchmark (" << heavy_size << " elements)...\n"; + seq_results.push_back(runHeavyArithmeticBenchmark(false, heavy_size, instance_type)); + par_results.push_back(runHeavyArithmeticBenchmark(true, heavy_size, instance_type)); + + auto total_end = std::chrono::high_resolution_clock::now(); + std::chrono::duration total_duration = total_end - total_start; + + // Output comprehensive JSON results + if (json_output) { + auto now = std::chrono::system_clock::now(); + auto time_t = std::chrono::system_clock::to_time_t(now); + + std::cout << "{\n"; + std::cout << " \"benchmark_suite\": \"comprehensive_openmp_comparison\",\n"; + std::cout << " \"total_runtime_seconds\": " << std::fixed << std::setprecision(2) << total_duration.count() << ",\n"; + std::cout << " \"benchmarks\": {\n"; + + for (size_t i = 0; i < par_results.size(); ++i) { + const auto& seq = seq_results[i]; + const auto& par = par_results[i]; + double speedup = seq.execution_time / par.execution_time; + + std::cout << " \"" << par.benchmark_name << "\": {\n"; + std::cout << " \"sequential_time\": " << std::fixed << std::setprecision(3) << seq.execution_time << ",\n"; + std::cout << " \"parallel_time\": " << std::fixed << std::setprecision(3) << par.execution_time << ",\n"; + std::cout << " \"speedup\": " << std::fixed << std::setprecision(2) << speedup << ",\n"; + std::cout << " \"efficiency_percent\": " << std::fixed << std::setprecision(1) << (speedup / par.thread_count * 100.0) << ",\n"; + std::cout << " \"problem_size\": " << par.problem_size << ",\n"; + std::cout << " \"gflops\": " << std::fixed << std::setprecision(2) << (par.flops / par.execution_time / 1000000000.0) << "\n"; + std::cout << " }" << (i < par_results.size() - 1 ? "," : "") << "\n"; + } + + std::cout << " },\n"; + std::cout << " \"hardware_info\": {\n"; + std::cout << " \"detected_cores\": " << max_threads << ",\n"; + std::cout << " \"threads_used\": " << max_threads << ",\n"; + std::cout << " \"instance_type\": \"" << instance_type << "\"\n"; + std::cout << " },\n"; + std::cout << " \"performance_unit\": \"gflops\",\n"; + std::cout << " \"timestamp\": \"" << std::put_time(std::gmtime(&time_t), "%Y-%m-%dT%H:%M:%SZ") << "\",\n"; +#ifdef _OPENMP + std::cout << " \"openmp_version\": \"" << _OPENMP << "\",\n"; +#else + std::cout << " \"openmp_version\": \"not_available\",\n"; +#endif + std::cout << " \"cpp_standard\": \"C++20\"\n"; + std::cout << "}\n"; + } else { + // Detailed console output + std::cout << "\n" << std::string(70, '=') << "\n"; + std::cout << " COMPREHENSIVE BENCHMARK RESULTS\n"; + std::cout << std::string(70, '=') << "\n"; + std::cout << "Total execution time: " << std::fixed << std::setprecision(1) << total_duration.count() << " seconds (" << (total_duration.count()/60.0) << " minutes)\n\n"; + + for (size_t i = 0; i < par_results.size(); ++i) { + const auto& seq = seq_results[i]; + const auto& par = par_results[i]; + double speedup = seq.execution_time / par.execution_time; + double efficiency = speedup / par.thread_count * 100.0; + + std::cout << "--- " << par.benchmark_name << " Benchmark ---\n"; + std::cout << "Sequential: " << std::fixed << std::setprecision(2) << seq.execution_time << "s, "; + std::cout << "Parallel: " << std::fixed << std::setprecision(2) << par.execution_time << "s\n"; + std::cout << "Speedup: " << std::fixed << std::setprecision(2) << speedup << "x, "; + std::cout << "Efficiency: " << std::fixed << std::setprecision(1) << efficiency << "%\n"; + std::cout << "GFLOPS: " << std::fixed << std::setprecision(2) << (par.flops / par.execution_time / 1000000000.0) << "\n\n"; + } + + std::cout << "Instance: " << instance_type << ", Threads: " << max_threads << "\n"; + std::cout << std::string(70, '=') << "\n"; + } + } else { + // Run individual benchmark type + BenchmarkResult seq_result, par_result; + + switch (benchmark_type) { + case BenchmarkType::SIMPLE: + if (!json_output) std::cout << "Running Simple Benchmark...\n"; + seq_result = runSimpleBenchmark(false, simple_size, instance_type); + par_result = runSimpleBenchmark(true, simple_size, instance_type); + break; + case BenchmarkType::MATH: + if (!json_output) std::cout << "Running Math Functions Benchmark...\n"; + seq_result = runMathBenchmark(false, math_size, instance_type); + par_result = runMathBenchmark(true, math_size, instance_type); + break; + case BenchmarkType::MATRIX: + if (!json_output) std::cout << "Running Matrix Multiplication Benchmark...\n"; + seq_result = runMatrixBenchmark(false, matrix_size, instance_type); + par_result = runMatrixBenchmark(true, matrix_size, instance_type); + break; + case BenchmarkType::HEAVY: + if (!json_output) std::cout << "Running Heavy Arithmetic Benchmark...\n"; + seq_result = runHeavyArithmeticBenchmark(false, heavy_size, instance_type); + par_result = runHeavyArithmeticBenchmark(true, heavy_size, instance_type); + break; + default: + break; + } + + // Calculate comparison metrics + ComparisonResult comparison; + comparison.sequential = seq_result; + comparison.parallel = par_result; + comparison.speedup = seq_result.execution_time / par_result.execution_time; + comparison.efficiency = (comparison.speedup / par_result.thread_count) * 100.0; + + if (json_output) { + printJsonComparison(comparison); + } else { + std::cout << "\n======================================\n"; + std::cout << " PERFORMANCE COMPARISON \n"; + std::cout << "======================================\n"; + + std::cout << "\n--- Sequential Results ---\n"; + std::cout << "Execution time: " << std::fixed << std::setprecision(6) << seq_result.execution_time << " seconds\n"; + std::cout << "Problem size: " << seq_result.problem_size << " elements\n"; + std::cout << "GFLOPS: " << std::fixed << std::setprecision(2) << (seq_result.flops / seq_result.execution_time / 1000000000.0) << "\n"; + + std::cout << "\n--- Parallel Results ---\n"; + std::cout << "Execution time: " << std::fixed << std::setprecision(6) << par_result.execution_time << " seconds\n"; + std::cout << "Problem size: " << par_result.problem_size << " elements\n"; + std::cout << "Threads used: " << par_result.thread_count << "\n"; + std::cout << "GFLOPS: " << std::fixed << std::setprecision(2) << (par_result.flops / par_result.execution_time / 1000000000.0) << "\n"; + + std::cout << "\n--- Comparison Metrics ---\n"; + std::cout << "Speedup: " << std::fixed << std::setprecision(2) << comparison.speedup << "x\n"; + std::cout << "Efficiency: " << std::fixed << std::setprecision(2) << comparison.efficiency << "%\n"; + + std::cout << "\n--- Performance Analysis ---\n"; + if (comparison.speedup > 1.0) { + std::cout << "✓ Parallel version is " << std::fixed << std::setprecision(2) << comparison.speedup << "x faster\n"; + } else { + std::cout << "✗ Parallel version is slower (overhead dominates)\n"; + } + + if (comparison.efficiency > 70.0) { + std::cout << "✓ Excellent parallel efficiency (" << std::fixed << std::setprecision(1) << comparison.efficiency << "%)\n"; + } else if (comparison.efficiency > 50.0) { + std::cout << "~ Good parallel efficiency (" << std::fixed << std::setprecision(1) << comparison.efficiency << "%)\n"; + } else { + std::cout << "! Poor parallel efficiency (" << std::fixed << std::setprecision(1) << comparison.efficiency << "%)\n"; + } + + std::cout << "\nInstance: " << instance_type << ", Threads: " << max_threads << "\n"; + std::cout << "======================================\n"; + } + } + + return 0; +} diff --git a/typescript/batch-ecr-openmp/src/openmp/openmp_benchmark b/typescript/batch-ecr-openmp/src/openmp/openmp_benchmark new file mode 100644 index 0000000000000000000000000000000000000000..60ed5893e6b52fefe06325376375077139f26b29 GIT binary patch literal 60720 zcmeFa3wTu3)jxbDnZR(%Nfb5S>PQDoluN?R1hLK}ndA&iAOb3QOJW#EYHnsG5WEqX z1egwksoH9*ujOs6rL9)pYAvFMgg`*>f_S4^E26>~qa~t+h)Vvy-#+I|PD0|__x+yl zdH&D$F`C(Duf6u#Yp=cbT5DgJMOlg$l zl*ULGOG5y$;?F5cm}*Z(x<5`UFjAY27mt+nQurWJXP@)}k&`4NH8^C-dc7Z~{-~v? zDO%P@r6euSdI@~A^9vulQzS-OaJ!Z@Qgc1g-Uh8)+The4^mN_1TGmL-^(dRQFKXp$ zUvY{DBOR%CqJ~KP=zLOK1Z~pmZPM!*X%Z3eF;YXGB*(u@U7nfoPUM)^Qti8UochoR z{Mpw^aSc){VY(PGhb$sL3^0D;^t@3{+|l@Bp7rPdu=$r#q$>Gg^|BSWJ-_hdbqih^ zPaO7xJQ0T<;_EEO@>B6d9Lgt2n&g~%@n|<;)9{yxzp<4gb4I`Z>iN$rH~ePNW5FRU zzq|Lc+9vP$<6I+uJNluKIrW~~o=tPVsH=L`a0 zIS9OK5c($vfj_8%`LjXbV+P?TItcxr4g%jj$hglKq}}6#(D{!+=mmTHwgTG=t;74zI0%pQ?Qyu0{B!wo@1m$DQUe^hMiX8hni?m81rgUo4$1ZP5KW<-*U4fRp~Fl}?d|*u|#~--I`;c8a_a zqTLGmswOp5BO^57vxk_SNZ0b+*;w&$_6gEVfMVhw2JD|k_umLhI^@0bi=&Br1YYqKv`|| zvZ4xK)sjG2k*~J4y0&muDN2EUMNv^{eSJno#-v+I{N<(KptihfiKld4AS0u+tfaOm zP+L+S@Ow(#^GfjL&MGP>D)IZvmsGju&YCv`tb$7QG{3JZP`g}j6lAkBYUS0ac`7OQ z)Y1xHNv#_tlZrgme*OhY0_YnU&d98*DlMs7QWhxk)tCBe0_D|J?8>5w!ljiO)6+`P zp{nZ1Mb}NrnB-p$5mgqIqTjbouBu)p`VF2c{OH7W!K2E2+G-ZgEjL2Dc=qo zJ#TW+T#$eyiy%{9Y2mC%fl3VDylhSOy+u7V(rZ&0MLp#2;XtCH3`1HNIMZbyZ14d0=_b z()1Xz-bq7P11SNzl`&b<6CK+#zi57Xk$c{>qQY6|cZHxfDbw6^No{S(@}erAq0ZEd z*=F%)OvVri`J?UffIbT<{2sK{Popt@r*Kd@AMH;?`y9NgWmGt5bDiH;w78_y7XX$2 z?Px;N<`+TRj2TMCO#>@N5Bg|~MXgT^l7{uanWdF=oKo&eDvQ>QDW%nQ0nwqFih(pv zo8e$;kmkhb%+DxtyD>ad*_TY{pwJv&)gqJLWMmmbR#CkKM& zNm<(Cf(4*7WkN=Vl(S%gE87DylP6@EGn4utqFygzpOStA`()`MS z4*zN2Hfb12h9f^RRx%1tiNKXjm_xAQiWAVGpphsj#G#%AUV1nvB;v^eD8DVz2=N^U zOg)Sc`7FfbMx0c5$?_5O@Yevw59^N8%hQqH`@>5R0VGKab(qt^e4=z~3_o7VH_Cwj zz)ydU+(_xiI_w#dw@Gz+zV5toEK!F_4aoDu_QvRCY%e76hfC}F;9E`jHt9^gjP*;p zA4Jz==|LT~{Pm~MpHb4|dcIcVM@UZ_3@S;*Cio~5yvzib zP4F5M+=#7NTyKIWoA4V<@H0&CwI=wPCU~<6&V3u>^NNhbJO6Z}Ue`02;f5;!e^(-Jr>fzuK=ErI{ zEYVFO{|Dx|REcg7`Cl{7B}%keGQ6!{Z>LY_;4s8i&RGS8(zG+pFBWu8lbs9od_ zGSARInk4e?GS84dDvA6)<{9coyS`)pcQbEi{(#7DW1gXXbg#&7VV)s1M>{wqZ>s2*UU3?k2Z_^FPUe^9&Hf$` z?F3pS+O0a;c{*Al(TZbeZ|G=O3bap@&`xy|+E+sVB<-^*B}t_|ctf9bene(c)YoqY zTagzW8^EjjK(WT7qW(mGa;gn{Ld&XFk0fvICAqBv)xJZk8p+TOL0<{K;AoIY>lsHg zQ=_+&1?Dj?=%td?NG_NUBR$x5OU<`emNPe|eeHAhI`i2BWqtbAOZT%=V>QPc3$ zxs`-JKw0NOKE1`K{e0TPr&syZ#;5Ik`YWHd@aaiDZ8G=rH+_0}Gg|L#CT5VBt4)}4 zV;xkn5B5Z0M~ehBCN{Zoc7HS*(J}#*FOpo>yRLIx>zaS;P9b$1F`o{GFU5*lY!a*5#ww=I>F|c9v=uZRlN%SqY*CiWzV7Mqgmx&wZdqP49i;GNk(*NRiKRxuj=^}j1|jy ziHP`_dOj5?6gw5&t(TOYjlzPUj?|&GAz+-+`~b#w1gwh>_yESX1k5-I2aN3rSUMl@ z0gP=*!R|P@@kMkz*c~r7PUC}WksD{gUF!l^3r`oG?{Fv)uOmIQAnd%i0bOZ z$ruC;f*}TkruGl;Ju!^R1*YmPY?4KLS@|fW?1jGW6~Jc%5VM{tOpBu4s?!n3V?l?K z>PE>?IVsn%K#2q(e8{K^Abu!N-Zmgw3MUF05_t+*OL48CGw1}bT5cLsxRt5P4iw59aT3Hh&f|Onra9kHad2ZQHV)&9}N_u zC1J1x0vb{P5SaUk35;R^K*<6;sB`qzG(CR7L9waRbs&|Q#3?&l_CJAGfp}l@}&lzKRvud0@zmGiiljc4wq& zt&nxK=UNST94Q^h5s@BN4-+09#DVL~h(b1pr7p*A$@l0Rn!Ky!sh|Us8M9cyTLXa-L1u zGJgE6zN*r)O3&o8m0^49&h$j`yPl&2l&oFyx*K3?nhR)()Dv{n+giTc=%kVJ!Y$j> zxUT?&y4i@)p<`SOD@F_b$(KDnfH&<2+-yIuReAl>0J%c1(70~I;}(cgYDop9 zt*zMYRG&jbO4#ZUIVGH~E+8PZvn8D-a~3i!Gh)ayn3ew1s5_onXDs^zW}9PKnb`xe ztR#?sZPfh&eQ0TjWe+l&7i;JZW;e%5w=>%n%l?Jg^xqhy9%r^JmVJ=fhho{a$kI{M zDJfa~LzADdbmevx3Y8mDZ@k%clj~;JEv}+=;Wz1Ugr9l78&3Lu_>aW`PJh;TH6o|Z z8j*)HENJ>%Zu}Hz(9=$M-3AFCB~ouu&p{J(mBnO$^G>8Yy`fJT=skCo>}k;GA5fp` zOmFB!%PX2XsNG-m^b~};sRtiI%UJYgucoA^`%z9!Ng+llCLA6;-jE*WgabC4 z2q_A%5~IR8K-Cl4u7wnzbFPSxg4fCPkb?Dv)e>|b6Gjhlg|M$x4?jF|H;y8dF4FQeg&j~3+yJa{kWE^vcK7&QG+0+}w@MsYh?8La}TgTAK z1o@&y;DO-Edu*_0cgRh1LCMu{&q%bLueJSU%=Sd40-|g|=(GZVh`sj8>&E~@xupUS zJqsDFpAGle{0RM!2lvNg+((80qC5xO75D>%UGn-bVO*#v^f!iNoZ>7$@r{pu>i|h4Eh#58lMMkHO}bL4AnV(}XyKj3gg7k1 z-uHoJ$O|eF-4B!M4IPbj@|DlEPIiKjCz5vp8j^}Ft*4{!>Xf0sQs-kW#g7}N^EOay zX^v%YVs>*ZTfpoqe>9MPgzT0iO7+ti><+-Nd**+X!(6B;E4R_BD4}=MF2s_W@7G+* z&TJb;MD1n`_?K@$L+u2|TM|h!;R&OeOM!r8@gmJXWeK_MY#EJm^#!8ai2iVlgKogU z!59B=3I|mh2UA;?OPaG2!41P8L?a9;!4f5cTnB=UDGrtWCvP*!+Z+&XxhTe#lWq6w z{asqyb*Hv{^ao%^@3QEKXqy3$dYqLE0et$|sRFnJ2$&Tg16h;7RGlp$gUzuT(__4S z1sG}^h}DQUcVsnd^pn~)z#z5fA2~&8nL6i`OfTnJB&Wx45SZ1WPSpD$gf#~AT0Aj^ z&SaCrK%wRC*g(zEnp940a>s?5u=+N+mqFfcR)Vk=bNFBE1XUPnsU;kv`ZZwa$kB&S z=}7f}R2fjJ0iEhY9OosV&~hNA^b3#`gT?k2%M%mwm%-|<{u17D3!H(}QXcE&-CEmA zPi_0l3kK~`H!Gn>tJtH?PxZ#g78}cEV{0PlBy%&6+X7N(e=$HU&9SEAw5GrL^UdN_u(DarO?9lEQ)h7U>wa(h-;?k4OG_p{s>^~rk48?I>V7GL>)DQ=!oH^mV+_$O7hoFdr@=a0iBm+96rQYbfR#t?vEp`xX*&HkAQ+YiQAPwveXe5PHw7U1}y*$c6 z=~HPKDn%~_16%JznfhZgpzpyfxU7q@#i8iCkbsfQP^l%dL1( zv#_gW0@zjGMxVK1MW-80#EikthSbtXRK zhgboUOz=3k{sO^Pi)6@By&BC#=fIQdnqH!OO|XXJHi^1^A`rL( z3-7IObKD6F!a0+Z&#q&WMbts(#cqju6Kfc|B;3nBqIyt+!bwJr=wX<3z4brh&?XnF z+;RF|4Hw^q5VL4HH*`WR#0zlTF%UpR#u5qXNNRf?GrDGw57f{$Y(hJsH0qhC+{?&6 z4F1W`=5xr<2WkUIM6c^*$O4A6E=eE$DHw>9{28hi}QkzAH*V;R$^J6MUDbX*j5NHcKr@zcsAs z?2oZ)3(Dh!;m8f0RBwVXx6o0wyo5L%J;EYUy_9I7lhl$KD5vrKhNfdH#MDRQLr19R zW!RRC@!LsffKGgrgS5Xaend=-+jbrAdEn`?cxRYiMW4nb^YZ0@8M3%nud~LW-FNP8 zAg)H;r(@m>Om+W>P++!A$2UU zQJ*9cu8>=%f{3m@D&!5_5fmWV1eJ$@L6U|dIf17x1%^B0IBR*~edNK#0nHb(M2zz`k3*?bDgYe-T~%CYsOc!N$c?g>yVAjM&nLT?lw(kQMv zm0}OvE+|42eMt`0Nv^l zWNnvMJ%@F^CiguMnxL(giuU^k(sRfs46f7?3#7NJ(*Zf-i*1=d#jrmC3?2PP_>_*`2#y4i!DjyrIwxWZ z6_B>jo2@b0A;7Q~+S9TJ5sM(@%{s7o7Hpd$=XI&)v1wxpjw0^&SP#TIG$7Y~2L&Et zkW=4z7ug-L26rK=iQ~N(%nHKcoAh;Q=pX86v-}=p1zxdgIU1|*24JCoXyQApvnVw0 z$1&Gk`i~79HHFkKJu_antZ6#;0{;o%w=~31|AX0wW7+k{M$h>M zBrhXLXUqoe5Y@0)(**LiU~Xp%7q99Fmea^<pu{K|MXD zr|;_NXgyW6)DU0vZXMo)l=LR)`Q3W@vYx)Gr+f8upPqiIr=RKR2uw%qTo89lw|?;k zI(rZ-Z?U1I4@rtEGdHp00ArhX{3D|lNEb~5*QJ?KMT=mk@$a%; zm)D&Mospl~m2oUGV|Z@FnwbmbjeJJV!;`(?#7Ip~@MN64`ddOuUs^0z9T9G*bf03` zy;!b!3tp*uY+eby)5)_YAoWB>{xr0$?bEnOEjGfgNmAMlSryCMox}9;+LnYG+kpM% zV!5`MFP8oLZA$3%#qyhb2x!H_E+y;jx>tbS7HSja!4JPB*zy_|bGx&4 z1#*D|I&#^jShhv-ag5q}&krN6I9Dh)!>T+afN=yS2HUKzkT=7I z4xtxH=IUX7NIv=2Xd3Vec`W9jhewM1vrZ9J? z%icoL?h4~p6{Y%yyl$+JDmrxpJKb1-THeTwj7!PQ>Xakz2_8F@wol@5AC1CC)w*r!MbM~p5|kvGxQCyhmA*|j!hP*+Jb1|L7;6{p9KRb61CXkf>B=; zi3sK+3Mh#b<;$sriVw79&8NQxIR1ztSJs9w3h>RtV@yqE2Hj4 z2s%}}2hiTE$@z65=Tw9%=x>Vp&OW`rDd1sLhUg0V3&ly@rvCCxQ5X=c1q5&x3Ob?- z(7F=Z*15(sC7Mn5{eqvR)e)Zbkg$Ui~Q8T&$Q zq5ZDo{|dEZeylnKO!=uj5o>Z}bUe;+gMR=ZW1kW(kldk9f}IxE@voF{7V($jVzz92Gxyfh~= zatK4&VNg0)1bme65GB|K-Rwv?nk^?;A^U}{Zb)?(@REmlvUb<~9eQWY&3Z3zW^fyn z{>^*NcVU=1|3jZ|+fz(;E+}uFJV5pLDv{B1!Oc71>TRg^Zd8KGCqarP0lKdBguWBk zC4-+S>Mf8D_Ab|8d_rxmup8?B4UNcp&4@sm--}$33{9t^&jiq0`jMyY{C1eARk z)Uk_h7V+#c1g~$PH)rJ{kFG(TCqQSC9S(U-4(elIKZTlp4p4hr<(gqooClFcq}v1crg-b);er0kr+7d=s=9 zD1sS$qpbyP80TawpY|iNwQ*kU#Pslluf?<~5VHyb zm1*;ecH}1ZS6GwfS_cF`GYzyQ(O&_V)92A<)9^Qo@yB5^t=^k-CSM9?@|RbM{H~IP zJHI;+mhV-X_61~b=nw;>jVNqtt+Oj>otvReE0mSrFj*nv0V>_FnO|~2yJm2CAE>?A z(091F??fw+oFwhNk!ec!Y9*^9aDx&lz}%W=Q?gzQOjSa4m>U3uuEoWNc}$ad4St*w zx>5!B#BiXJyB^5cMS&*JH#LXzQ^tOSqTnuTC_kkg z~3*n+JDNxc`ewq)U{#Ti@c+)0>Nusr!Y zW1+zImGJ!sfGL&=O8DvhtUrgiT&at-4kBmuDnhUihwBV~Flwg(7F+4gz$lBx#ZCvS z43AaW8EfzuI-{)+-;Kcz0tS81)+$YV`jcsA{~OSaEjaklS5{8}hA}mE1j7lB-bD-V zRKv~td8cD%!!9I4fWPvf806j7kSI~dk}1k4#%3w$Z$hWUiK@oM9KggA8>vTF`6JL5 zo2d9oICsGSjge;m#x{dY-^Pxydt$-+MQ{HB3;VTgL-k**0(&iqoiU2BomCPD>0Ng} zMVQ!07#UkaK1`U{i|7^Y8o)574XCYk?2_F;>eKy2Kz>__OJbP>@Fq|5g*@PHE!DAg39G^jOcX6HUZt>}%Nw{Zy_DHW;nT zKij4Rw^?=2=3lc|c6X@`Qi(}X)3PMO`pNOzU4Yp^&9wcZ5++7zn6ZJNz6lsSg&l_x zDH^qH#Oh;=HW4D!zjxc_5!S?HC6b+r_f6zR#>z@)Hi0Odjksr~T?u6aAy2_xr5(jAw|8X2 z4TbV>DP|^IVYVVFhqXci6(A`{ou~|iJiMro?L;vvJ3F!!R$hSe1*lwx%7J_o5KxA+ z3~4cWu19TNNh{WC0+^gv-;rGg7g%j;X#Y~Ko|!5q&rXfbhbNA{K2E}Ny84DPIr)~d z&MkPHtH{aMDN#R4jd(?Sn+Tp(TWDjy|Cc+c30Hc!$c4+Rvp?QNN5gvqGZJH?Xx3OCKhBJs(nd`xO+TVJC^EI z@9V8Pawn?d@?+NLOWmG`b(|-w!+&!@)>pN6d(yrwNc+qanKDkXoYb29omNkMP-D4s zy@#(HeTaM0JgELaeJY(iUnx4Qg$4+Fu8b~1|2NZwM7iK2slbOi=Mjzbs0#&p4e?rS zLE0f)E4BpOxKbQQ#EnWs5^6D#dB*-J*6tI%mgC;Eqaye^gVE$_Rnpjseua34uSJQ* z47zq{iK~Eh9_oI)I4NyiObpex!m+7rdiW_^7>U)3KI*~;5YH|`u~ER3B8N0 zjk8Y70X)>jjR0(#a5tqJkl?2lrR|_qiOfGciSa!5Oq5_NmcaVHiP$uelbxuDjgx-6 zCVT5%6T2pVGw+&kwXpkGdwjsDlzttt4hvaZybc zp)4#o_j6b9lQ`T~AxTBf-OarVrw4nhBMP!A9E!Z^7PNxp#q z9Bg9nP`45qikBK701F=H5A9acjw@+j=UR>g4|fN9Y_(@2&1em63$|i+z?$q0SrSC5@sYVtsZ}{j7~M zc>4mJ-x+2GuV$F+zcHk0ZEH-Y8=s{VFrY~p`!ZhI+AzzB9Ii8lZo*5XsF2BV;|xky zXXyHo8QqC9n8nTN!vI9HF^RQ(HI5%7p=KOYU9h*&gPiNocfuEFx5b`gr%`?}%SQ{_ zi}s*NDe6DA#&~L~pe^G3%6JLe80Vrj(!texzZV1Mja+Z@g!XuX2lX+-3mukjya|Lc z3w3+b_IlFVb1iD{KzFd)R(l>&Yzw=w$$^oZ6&^QYR%qOC>>bvA=E-_XUOfX`&Wu>o zur8H@o7mZ`FXUBU;lT?F8p*dQAP5?YWtS4h^68vh*toDa32k}LL2Em4KMo5aS0wi; zNo!JGrGjHThqggqI?$>To{`}R%}CGMTYDtBM|21Kkg$&l@|qrGm8&)jW%yD&3&mKo z5gcshBiauaV&|xJNGMtUtJ6vqiS7p`MDwF>DJV!J-zIqMr^4V)@C7|KSSvt9JY#3< z?clav#EG*%9kRL<=6wfXKKj6k#jxr)Zjv)hT#S#)~_N*c`YVLniLw zE91$i>qY7+_8mNF@Rj2%Vu^YOm#Fabw-13UVx?m(sKg;vLa%CXZ+#31-VLybH@NC% z!y`y*-BR>7>|5wHabI6;1rDU_frF|$TYlC#gcxrCLw?q>c4)K=3M1YrY6YnCr?9|DK_(-%H~VptUJPyY=_@QK*KYR40{k<=w<14v>= ziF;XpQHFM!1^&M)q z1GkErqa46eAEb3Jou{k{nL#aObykG_}<#NbG^bWVg zb_wsKdn>#gc}&sWlP3Z{;ECMiD1+A;^$x*?LG-|UXU04ieB8KJ&3$=b22XWgR)=tP zG{OiI#9*|-ikf`bNqDd6V|K)xnCoIV#DZT2bO!zi+Ju!_^jR1pgTA^4 zrO|08`}s4>M)mfw@K^7k#N^M|x#!q_lwZSg?)eEn#mGoa?8D4j4q(2t!M(2rTKHhje)2nWvtyRny0sM%Tgv3PmS zx5z73y(sjdOFRqRSo1q@u$hnaV@_+xB77I+1YLnO5yLPM;_2_lOj<1bnDuIt8=IxE zHjo=jXXA8ZC*j7f7H$mJs|RpnShxr`wg@}h(G%ogJo$9BaAbuW>xj9rI}JC6YxPHI z+b6P8BAYVY*oA;_S)sYHIffhiPZEY3BWd;PzcK?ic2aX=R=TlWz%aWp{JO+n0P6gy ze(v01sqAbLE1PQpj^+q0%B8&#r);>{qTUX=FsXQ?VcnBMa|>(9gU5Dt3N#U5Ep$Yv zMRSY4ozVy2{xpyH;j`pxN3K6hvI_6PdL^QsG< z$rfCHOw05Re9Kw)0{{z!CESFAkefJE9V%RYDlVvALr;oJs+8*ChVc~jLR;1D%%AcgO-*N7H?0>p)ELpy=mu5LCcqyugIBm)hV^z*DtjMNVG^=EWy zSaBsyJ<~u#rG-a<(4jVHbYRlZA&fM~7=^gs&XE>}Q}DU^u@t-|p4*P*;Be|Bo~#b| z93H+|9R*q6$xUy-l&G_{CUGtd@2Ws{FLCYH-@88K(!DF!h)MxmY#F`la)J%-$~EAC z-j%DS|IWK|fZJi<>0G%aaEetTmx#UI^&{LbZ1`qy=ZZ3uZ*6=p=2|^jCu6R)ie99T zYZZRw|2^00$$CY1tyX!p*hF3VHQ%*1Tq~btgvE5NWxkT79WLwgzU~$J4!za9E9@z} zYl85uh5fy2F}>?6Ph-xk+|DW7*TEY8kG`@PrII@L&%M*DaR&th_gX02YwH}RaIZC? zCmapHWu_^i{q7R$t! zH6o3`5jKzh7Nc?z#PvUNw^xA9Agjg;$jd42_5yv?$g>W8)u_4K57d8c!dM+52jj`M z>qQQM0bme=T8Bkd0VTaZl-Xy@zG|1@LCJ$ zsu5tAmy8LnwL_xM14Hw*w?SKkXW9r5F9b;)ma@)u!p|xIMQ57)tY+0y)M210tU8P- zta(!O@YjY5ee@|dw<8+B+|kC(b-hx$&2^n8w6phDszg`0WD!mj&KR~4=iFXLrrh)Z z_F)xVRJCD$=thV_TUp{h2|b-%;mR^?xaLK_Kw~(@g+8X^Ej{d1A~N>ubb~rY3Hz^L zQHkAXywQkjZArTS93fbQD`P8m+7iy*8djp$E#^M)gT%qD5Bg}8cuBb9hXpH+j4K>9 z0?nJ&DK}mZPL$2vkP$vByjNB@aH#MYfm!YHn!reQ1^vViz>&Fh5f|@tC$TLw%q?j- zxlU43MROxXbJ#|~vKTAx0{Gf9@Y4T3aV04Q3qX|uN>X3=6IjFc8a81Hx|ARmyOz2? z3ZHt|f=`5y8oGfh#Pbazqh9fNvhZ^!4nsK0M)VVo(kqzH zg-<<{6|jBNwDo7+LLs zQHW^y9_A6X^9f3!!cdJy2e-rcVb?YwW#GFMwY5zR%MLHJSJw|P_=LbWaH(EmGUY%N zPy;XS5jB2b0})ei!H%4a-d?P;B*e=zj}U_Fo2dro-l|*3VCZHs_%=)3Ulgq7i+NSI zfy{Bk5ZxdRT$#ZBC7z~_>Bc*(2w=z_x76gcyRZ&y10gqlmS+9+9H+2Gz#aOA_eYPp zf**a$_^}xK?pIxr$=hY$k%lcd)a?$vkAP}a2B2*!clf7H!y{w2A0Xu&u}FQ7E0Tjo z+wtl|yn?`rb?tz~DQ$-ns5|oo8_G@N^twUypN5^?13a0hz-n5sL z;MaJG_iacHnw*6Dn|ttxaOfWyxbau>5gw~ATzLh4A8B!}T#YM_)!T8f@Bua;PPTq2 zAKJSZw-ABX`cGt9k!h2YN84b$Y={wo>XJ_Yda`3M)ZsuK4s+6GM{rd4NN!}d#hvwm z-1sOM&W?=y0f!keNL%QO+_b+1+Ym)A+#zl~aYIIw#2}ZsvOc({JTh`JOT6f;U8~r) z9@y$v?+ka=;d?S%k&#nal$&;l)o2J9{$gt(*=mdA;pVBRh9))G@Z8X=o?vT=JL@&M z=|ADT)w_N#-Ul9s1Bec}X$hq0##Scuy*T4NZ}<^M1G+RTGH$Ln{Fu;oPuszG?P{my zKMTUS7h-E<$^{uO&5F#3YsRSK`0HJ72m<82Qg3)Tye|ySwiE+{b%Hw2zY)=qUT;mgl_2=zDJl zHt=pa>-=fd$z8dj*IrkHhfn54oN$G$7F<2F3OgG<8$ZQ@_0wUjVQ{^yATnBUQu*qy|&^n=t06OQ=#D85~G1k!P}VQ z5CG0g^+rlDsp7>>m=d|IObN|P!8I*AIw5uB;3p14rJ2Y(wLFh?Z^MCYPO+9RlOU6M zHo56-@Q4>vIp44_)nfci+~Iu36S)U~>0`EuO|-lu^*g+c0+%w*6TT-?903)CXFF$D zlymn558@zb+fY8g7yJm%?Y73&&5v$$;(kExE)RYjX1FKo7HprTQy|cXHAk$HG&5qA z`CB}z4kF{t+U?I*!pUMMoo5T!@DH5>XA}-3<0)_szD{`4u(kb$?9StuDGR~QkFu~kuf13s&d*drMa~Xu3TVz*;0@0yR2TnDSMBZVuEdB20%hNAz& zAj+fiGpXiuFmNgg{PqMQf$my@(0BvWf$2ZqkN%gBlm7b}eNoS#-%KJJ{a1;E?v~NR zL28`={16n8(Hd?BlJDYvbRQOU1>dX!qKKUM6SNw|y97n-vhXL-K*xOOhDi&j9~g@* zVy>>Yr*Jjg&DQwP#Rt3zRd*n|03ppzVd!>^b~R}a z5Z+|#RJyzt)DTnm4cJ)lj1UOH^WFG897^-QVJ?YeyU5qS zZ-eAj>oFJu+o7l*Mbu%TYaA*ecJ=CCqY2J;gn3OzffJRv7&9PFuBgN>L>rLRgz$_n z<0S?a7{A=e=tL5vJ=*3D?RKT@Hk>*3D|NU5*$r1}QL>_SFGbIUzGt*Xuf|gBRK~b$ z3f+P=ih30|9I$-(IqJm&mZ?AfPs$B5+!NaG4eiFUT%DpNWQBgNEefBb zsa5k>x&fu?T^OFI9b6&MLeMF;Pa0-B;XU2VV-khPNi*3#SWT*oKqpBN%ZdZI>tX zcPx`H5_<>Ry^(Q4#fftr_YQD}h(8s_RZY_d@bV&SU>9BtkslDTGw`jYZnSZj{5deN z(;l4$T`9nOC5?XvAexGtwng6ZD^_|{orcPuAZudNVTg+x_{G2!n+lyFQL~UW??lb$ zdW!Hdd~s*sKbiTIU%LflXS9$SQc1}30)k_6u?)v*KKg(LiGHM~$-o7bUW}Mk=kg|c zqmFh3&~(2R>M~va!2A;Txx*`Pu`UM}>vrJfxMR zpd^(g@Y?=Vy!F#ziQbJOVV`f*-?PFve`36&;|YC>=H9>&Is%rujl9Mmxw}=3J_?s` zcdH~fGGySQ`U*f$wAmY(5zjZ1lyHsewH#7bc5o`FWqPqXhhqjj{+`zTXqmX)aG6?Q z`NY-m;)lSM0{6J(mZk%E@`V3FhwQpOW1lw^o#zb~Y{W_v`%v-&+tjJ3y_(jMG+_}ROu~U1 zjh{fpdk>y)Qx{7@?hQ~ux$$?%G}LEGf!Vooi*sRasMWPrXAdt~bu^J_oEC{bL_IyR zanr<54>24LFS7?I&k1fW>tz*KvU*FhT8tMAa+xawV*w za~}phcFQpriWAh=Sv(H(q7^6cv<+EuW5ZYU4)j|kf6+QT^>i*jKhNj;r#XFqR zsqGaT?O{&2jX8w}&Z%42VEAs#DYr4F@W7H>tpsz*E#?&X#6$4&22py6&gkzAMpOGU zY88w+aeoL^=x%9hI`=j+{xr>a$j62Ed=&j9oZv}$*@a>&7R^D2ZpI5EDI#Vy-cLng zy@Gd5xMxv6NsT#>&&ZpHK2yq0p}Dy^$43`j7t`^zdG+H$2fBUTiDKI-*TJC7hpxD7*GYu*!i8 zpbfb8`7OK_ujj%uz(uHKXvrJC9E(+3PDk!oOr;>-&M{^+MB~Ky<;V4Rz|r7mM!%}f zF5r7JnvAYg%h`kQmDow+IVBA$F85)GOGq{Zvi>Bty`ggz>*hFw)>Mc)E5Y~n>urzdH#-=Ob+p$GkLrJ6}g`VFIJ;&H}l5D6x1yxW` zpNiD3gl-2yjRHhWmD`cl+X2Vcfy<8Oqz+dqmVv1Js8$(WTTUbesS}k^siq8-7oalI zdMDsm$hw?bW$Zh!@-KF>Lb(tuKCLWR-S*io< zNEH;M6oi&whkgY%@biV70e7M@ZrtYEiHz%{%dlI0(zt^!08SK?p&*l6P5FhM@CwMh z4xLJNgZIOMQJ!!G21FMNT>S z=CUZ2yP5gx?Q-(9N;H9aCGs=2t8M9L?U5TVfw;X9PpZOT1F9YIE78Y+VXV_b(6lh( z320Ba9&w|yOWEnvJ`-N9K?z3j_s<3>k8>{y4nQuhZZBS6F9FJaf)6Mm zyh2=0iD7nxmP2Axck*7}g&0b1nvCA#6$FT)BsH3Wj5hB9s)4v}FXPP**ahUvfK#qS z-Nb43^7gMM?E|mn#Fz3tR)^T$-e$oeRsf^1lzZQD8loa>rhND_60K%9g~^K*+7J)l z>j-YoG}$)g#RkyDX9b-gj#$7Yt`e&cpfdM4Fb@gC@BV~9DAFtj3&$)e(VlO}5BOV- z7b@z@qECKn51fll;!Cyn41Ot)aisC8di;Lkan~U{f-Y|yC7m)S zVx0o?pUBg*q*gTEemEnk~Qe=d|)pCyp7^+8oCLq^R(odS%Xt$+(0T>pw(gIW8M z+&CPi!r}gcvccO#jk_oe_Q6YujKs8qu|CtNuY>{F%qfe{3J=P$=%-|o@CK2*hA;cg ziQF{`ivEh+I0QM36vP?)D~2C;^Sz-jxF*6G`eF4E%t{X^iho=V@d*nKtK65NXv9BR zC-g&Hlf#i5Iv|eTr~H)a$mN%KNv4gAH|2vP@vrRl>IeA~%n5bE{pzja_1TNi#VG&X zFikJ##Pm`e;#&l@uL11bs@r|8v)2w4oF4m52)z*`;1+oGTd+EnG@8}t@5N%>_9To+ zaxc+$lI8a_(inITot&-#pAZ9&i|bD10SX zgfNeO%sQOmV2is3B)A3rkrqFSxKTUbdB5&89md~t5$}*A#H5Ki{W&dx(-Jr>fzuK= zErHV#I4yzG5;!e^(-Jr>fzuK=ErI_VC18=_`ml9tiNCzG$d7;Xvuero=|u%arPWo) z)s+V9Rdp2=_NwZDeQ8NW`69iR-9AS9H$L$%e-`my{v0#iKF066z0Ow^C@-lfTIw&V zDXA@~sPI*QR3)Cv{nb@t#to0v2L)ffue2^uUR_lbD6jMt`F&_~ksp*K38gi@+Qrqi zl_gcBzM{IS@&H>{vbdtU#@|auDlMrZYo)cml7P?d4=kEKeW|ZBP+e=UD5+iItF;Hp zN~-LYCG|!4#{qq*X-22*_VN7Z0)6(`_I{tj+Ui>?e3f=ln!3ohxTLNkFx`H|)O78` zYy0HS_$N~PlPM7YaH>`v{G)CDL#p{}%b} zfogl5-)EvwQWvNmzsMIrm+cU?PE6-7)};8SS?%)|7P`^bZ1GRCV%Y2)w&`HBvZlg! z7hm}07kIskZzwIl3-nAaX=t$q*#946MEeO3**SHAn!13!*5`-Z{PyxH`^URm`rbhzAa7v+^czrSRO&!}kE`DMNF_H1gc9l8}LtMmo%SPi8q zp+@=bsf$be0iQo$j2AnowKm>ffM*{7<)tMR_QiEorFs>AdDS?3X|;cx-G6&+V4S_C zdRc6IG}&nFqCUs85qH?*FaQ$vdli(IgA3E{hM1?CGxS6^xJ#qSjBRC>OG5n%K#GdMsS*Q`Q?++?Q?2; zRRx81cNLVs)VIhq=JV>x0~F>}|Hg;>9DL+{yGGNn4gNq$ZNOi&3^KPbtFFDxf3e^@ zr|?=x4qf3$V;nG}AzMvxVN;01&%e>^U?{^$52eG@VN-CE7VO3CoRu4Y%puJFcQ1+$1T24SaO8php%iuLqs{zBzU1VQW zU0GfQ-{RK{0q29ezSIZDRu!-tF3#>-ytury9F`SxQjE7Dr#>TWasomH?5VI%m?r^D zQS(F=B3xWj;rB^!!9EDbOQ-ds&3PZz;S7rR(}jVH%vlh4f128u?xqKtsFmg(fqN> z^sxog$IiO}ZH&F~g<1hfBSEk_^ry%>hYhyG#-` zm5suZgFa{S=-M0J<&-mR9mqiKxajGMZNe4&9G*Eu9Y~bt6q0 zjP%XJr8(NubZA-cJf9C|>IbNz8nVZ6=e(k9*Zdr%Xy%;kBG2r3^Qq89bitA`XWsnT zt^&6-fyz-A@J*n@sD*w?CCmKq=1YADaw?%;8nN+uVlGgifYswP%Y3mUa+0ez7JxpX zXB2L!Pj~Z@-G`_KMEz+(y)XcQ>Hs3OTIfQ*=nG{ri7^~Av`Q!u3VVk~1a81EN{rF! z5BJyje2eO+BL-&gj46W8E(K$z!Wu0G6ET|!tVe%3k-Mo{Wl)}FCAGey@~XwvKs8v^ zVj`%&@JR-ZF}g|=A&R2T31-HW>ynDavC@1>#vTh#?GzBPbx(^qZiiLC(7`GUhpLQ6 zS+6Px3o|bk3!@?e3aL=HHptG?q=Bb;6u4uy1Awu^rTjeaoWgl{9%rAg#eH}->`8x* zC&p43yWR%vbM@FuFyc0X?EbwNE}27Jv=P$~!+aX{u@60+CkCR6P)W4SxJI)U_5vR! zWvOmv>xYW~!sxo$&`R}o5unu$m&U@h-eb?Nu3j{VLA}^sSdE~)+LA!5YXHF1DTTiHeYJspHA-sWf{i7PgfmyMO2U03$=dJlm%rfWxvPzoxaPla zfB(h4f?Er(1{j}iJ?*-BvG~L}t5%=i_4UQWoRh`pjjPA!T+n%N+Id&sIr>leAKuZZ zrw@pFzE@sx2{O)mog$U&dYY=I3-uHi()nq)MoT4+({g_Nl{3KDB>-pE;=(ST%W(Jd z;~|oCFVdUv&g8R5-$eR8(gvF(U9_yHryJ?U`ktQ0kk&ua({sTvTo|umitEcyAXV_j#K%Z&zwPNsf-uXFW+JUe>P2e9yEj${ zc>f=JdNw3LT6m$8zqVYD*JhiMcH^f8Y~auK0{BC!wD&~ObN#4C)(pLu?$a40mzkT%O8=6;wSLejj?%*xY>zGk+|%{ly&jBiT2f2 zS7K@~Avduw{z}wJbtT$C1}NEyw(JoK&{h0ZU>v9Fb?;@}_3_z>_J}n*F?C&nD=~fb z5LaSma44w69UGpQ2?(eH1DFOWiF`}}jpAS7T2zb=H~B!LF+OVIH}vPjJz@&#)}W39 z`m+SjS|60YeaKHtkDJ-I2b2@_;3m}Lb#6^gxs=naM0?!Kn3!B6B#y_M!0iI=EiqiQ zq2cBm66JV&3%pcZ#mB4s;={frL604PH4!3YjN$CtxAf)xB#dT-u6Q{Y_V)DLz`7W? zU|gJMc%p6g2#g?gi66@yg!P}A%YaSJD)ie$&%F4XxJecnOE6m3(5o=*_!0}y5Z8F1>M zf0cM<-`2%JhO0SlLF+vbV!qIqnIoSZ2rdV;w2li$kQp)PyxH$u~ zq0g6^`17m-HsTsVJzE4C>9Bz*ExXBV@EA{@q%y#$o zX*V7H}dM$pm{BL&4&J23paz;4RQU)MuJ`Xwz{Wh&kunU08X!t4Ky}r zcVv9U&;fNK&yW#)`$ZU=)oi!%Ikr2}+%A3l0gTD~`wbtSm~^wSYIx5kzQFRMF(7bL z!YlFdk6RLU#K-?FE}=DE>WrI+=jY=IITR0!hph>3Sfwr2gjcQ7Rx43n5|B8_ayMFl zEqcW%!5WR+7`HP1=>%zS0xBV&GzaO=tnbDj8!4$HHJFu6vaAs4yVg(QdqznoM)fOW zzcwZuj{kF_^kkyBEPlDA`UU`=u_WxaOTURr_@mwOU0lK@yYyOo!Zy3~f;HiByXC=z zgnf4Dt%QUx?9v~GBs^-jyqcJ>&Mvi|L;O$*AqP^3`PsP%zpz`@pP%r6-SUU?6V})* zZRaPfvRmFc|A)YTp0Y@Px8UguOTsaWfrNgAFq3)4A5l5$2jJ_CtmrV}<`javZMo#`KKHn% z^Q6z*=-}gd>>%V}T{1_L-|6qu5;!e^|Nlv#W|mWQx_G9RI`w>iNf>taO!#zujQp5> z`EmX7S^e_)dVZuv2Emh4JdM)x&Yx=ebM^dM_%43V7vI`Cm5=A?`G@p!WBp>&;dqg# znvbpVP62_@6QAy3PLb+tNJ&;LZyW@~!yofQAI?vTmPlvmsgWS05+C(7r$`Y}iq9s* z8vGdRcAcDbjs}+M&vc3>1HShREpPC>QOCn}hxi!n<5IflPeZa(z;Q!fG^My zv~q#;f5F!#eSG?}GEE=%JUzWmPZ#UyQaxR*r$5)z$My7CJ?+rbxApXpo*vWFAs1`% zI9pFI(bH*qny07N>FHuUU8<+6_4Ma@`naAxtEU}$`nH}P($iylI%JIAzMfv9r_=N_ zPfxGY)5UtaR8Lpy>Cg4_aXo!jPdoJVZ9P4tr^obk$Pb+oKWFRdC3-qdPxJJYIQcm_ z)9tDGv#(9FXJky6Iw9RYDSgtE^eZy5?5T6HrLU9(^n&qI)9{Mu^eHtNlcr@!bFZ6@ zO?+`6x}>zUe_bj2y6L+2~@KBDs9%xWBqd#jj^-LdRoN?e3e|#;DOiGPP{a{+bRZm~ zEhGKk;u!ro|9kpn^rzr^`epE8q(bO}d|3duN?-K!FJ>PsD0$@n)ONiwk`z_ESd3^w zj$1%PVv{JW1+iXJ%(dW~P^!-hY@FARdT8Tnz?3 zTy#wYiNqWUo`(3r1vP>20mKh}(F8@w#gKS}grI@M-+NVWrn|R0>&c&Tl zyf2FUDhNby`srLsyvK>0zA+j86_>!O&R3;gv+&`0mm$wt_@x5||BQw6UL@X)TX>;p z@Gr9XB?~`h;m0lfDGT4WM*y;i^sa@!an$hPy&urrc)lsOu+PA+wtT*C;cGGY-!1&d zG5F1xXQ{q_v~c>ir}RS$zu?O*Ka*Z4ov?6O=L{LJ`U~LaVRb?O8=R6wS8oBH)Q<01 zJ~B3#2$y#)T*cN;pT-1Da>mzH^!wse?wMG**D(BZO4c>PpT5R$H7ObLF{w6bkj?}VE&(1@$~@qBcO*bC|O4d z!OMWtSc<3rq`>!KL3bte^Xze#NglF<-vB&`&%J=}#VYc#KN)#!yny-)7yYw4EcxeF>u=dqIBa0$?_jdfj^W2 z{~6$Of7(Z*bZrX$?SPX!?t?DlnJf0A zQt+P!d~jf|`i#{Z6X9}-;XB*qr4&A=Q{aPGa3{6z5a1UNT%_W2#dQQ1Awqec@nN42 zxeP^jaG<0HsCyt z?so}Y!TfT1I0gSi3jCDiv-eS#>1pqt6F&5)pUYZj$bvtG53VHn4+2i|+-Lgix6!@tW_)979e-eCv2C-`;G}*ra9~}6idLlNi-%H^$4;D%FdLRYQn~>Pq@;2WrNHk=fgewSZ&~=(l`a+6 zOWP^7Y|?1l@eaE0%HbysU;VlwNFShl%kx7$1hm;x4l#dBGOmkGsn_ zw;znzwYTrfyt^gL&78eX_miI_eEht^OeNdscFL^Vb7-7$(K}Kc)gd0vSg(4<$&FD$ z7{y?0v9VsO&KHA;hTqlb1@PDW7&1)d^2HJDQC;R|a0e$)xF5pdaan3LZoJjP_EPh()NkqpIH3@lo-c)!iK$mg6VW zc}3ja9CFOl*dJELZL>!X+%l`-s=c7|8fIg18F5&_Avh`bJuByDobk{TyLdv6#b9xNI8V`SvI!OG$ycEYN)POaWR1Pn}6OIMnHyViyEtNOa$^cUdwTSbsmSC?1T6%DT@ zVu-5L1CucHBDm{vr&He0aMbN=sKpMv&~9?6aZ3w?jQvefb0{)ql+&dFh0GpMm5DtC{x{WUK_kaZ>v z&Dq*}Cl`*D^I8@7skemLd8;K|9M=j&2+D})9syaE)^Y?y`t--U+>J@;VF7tf!1c`yOMStz z9xUlvr{i~Evm@x>h3U-MA6^>h)kbYCX?@^14)&lWbczhDf_U9p5EV~XyS_%)R<&6R z^@r6m#%jMiaF9W5%JDFTL{w}V3;HsR)%5F#>P(hB($}yeLeQh)fsSIEjRgt@g|vwE z2=(U?jI1g`257e(!6k%(-FLm*0O`?B#QvJrfQ1Y7KDI;c+L|_P6KTS1ZcJ0h66StgUtgz4Fsym; zGXuefUL6iANCcut{vG34m{zcMCqV95fOTa60n zeH4XDmdiI~!OU^oD~GpctzQb)8E~pBDGg#Ypupz4ki2sWMVMpHgU zFMbavEPW=5zJvY_ynhcqY9jx(Eg#~B;0mdPk=kdxp^PVdQw zPoBk(?>_81Q!0?qNF+a1MdT+KrSIX7sw(v2H~1QDHX}W4vR?T@FZF*M?}`3^#S=fo z^-Byu{1!=m^1Y1H{}eD1QR=5ME}oDwe8o<9G{yVR3A_%|XDodtW9T?GF~q{E6=ydl z{Tx}CUi?Q2`^@_@EsJO~p%dsYaTBJO^IZ1_1_Q@UkrbEzIlLu$(Z8JEt1UxzI>sPg z{%>RS;vaFE1Vg%sn1@ol{Fm`AT>dpDUB2wzDFtZ8_ra- zS26P(FaKWv;|Q#G9xVPQ-=SaJMvB*0`j6!A00&1?J$mtPdBV~Q9`T|i&ys%#7|CzV zMejU&E2aFnJoM|>)OUqm{7{}1dRu=Qf0U$L;d2S-!{u+k>N4K;?>JK#ll87bFFE-| zh3UoL<+P=z_|b3@I?1ny(aZVm<_m@@-d-_w>S_Rpm(*X*ksq`46S4Mp!@}VHI?xl( z;TT-#voSc~Qj(nf*Qo3`{fjR!nz~sK&0~*Fz|**qwjq@F-*pY-Nh>3%MGLbBh4ahhH0 z14tbHog#&yyfeZ>F*3mj;}sqSk}gR}PKtGk6eAoXVwz@z)QJ~E(sf@>bt}W9!7^%u zTB3}T&dOUme{k8IA~3?hFJ#mR&2&QECYi2na>^SOx`xT95t`}fn@xXJ=r1|N%_bGP zmEBtnW0anB7wOs*U7Nx;!bBqAGD5?jWW)a)#WyF`i5TNDl)v5i6iL^h=o%Eh5$2N# zE+eE2Ui1IyFa3Aq6)U><2PL2Q2c6=^2xnL%+z1VMmxGRe`R`uJo!UGlueW?yr2J`w zO1w6$qI_|7=Cq2^sTJi_^$k-Sa{!-?g>`P&E9*y})PpB6nvm_`mK&|9gAE z|DYH66TQGU^uqt)Ui9qjg^#TlKIOf@-{^(UKYH==(q8cY&7@FgG^uO(7ZiVxHlSE+(8y2~lz7Rcju9DD|AqcrVRr-&yi_$h>} zFPp|mxJbm$WeR>g0x#Su^Gonkkn5T2DXFU~Us6>tZ_fNoXm(9$ zMe(vq$t~LpNmbPuo-5NG>2)ihYNf|pQSJR%MpgClf@K1!x}vV2T;7pcc~zaS)?ZXw zP%v+P^#by{bk2N#me;?c#s`5prPZG0<)yx=f`3!t{B$MPpHp64=Yf^J%%ZAtf4;+j z|4&`uNQ2t7zM6^>udkr860VfiSDVaf=r#wQganxdi%Uv9B^7W8U4evbbAq>|&JVa} zNkfCD##dWcT~$(1?qA_qmKH*;M)vqhDh;Pa$t8Hv@5qpOsj!O{cow923g%~fis#JF z@Ki|OXVldDrBBW7c4WXd;cv(&_ZwN3RaQ}7w^YQcmaDuDcjv5s+np*S=KC|f)%AX% z-2b>#I?|!Gx4H`MsrUK|D!p2DWsPTv&+n-$X`t_YC8c#Cn2dX>>MOOv%jOn)YD#L6 zDZYvj0#ZUmo-3-Bmsgc~=K1RCD}7p}ud=dw87iFzBMnv;*3Zh4@+v^Q4JDouzptU( zPqaFp2v8$5sD;X>K_6e$GNySeYeZ19LIayRf2nuM6b@3d8LbT6ErAt`I0#3Vt_soj(LO;ZKz(?<_SnxblpLse6@JOd z-$3ntT9t~gc=a*p8?XIR#RE4^MtYp~YZWgS__5js757iR9Yg#W?LowO?SI)RLXHvp zi#z%AiU~g21V3bgpKXF4F~QFx*C2g^75)3C{d6u0#{u=p&hy zWP&S`1c}`Qr)=X&F~MoGaiy8y)XBJVOmJgv#5AV~Zj9v#cbVYGn9x;hf)5NqHEp2@ zKF9=LWP&G{;7d(#n+aZHf*WOr$qgpBngR=|MiYF9iO(7n{45iEoe9qQiE(W(!PS&k zByBRmha1QUx0v7~Oz^EHc#;X;W`dtm5oLf0!MxUqJ?#6u?d*bug+ z9WlXEOz<!F69Kbiul8fa|tK7NyIlZ&ZV2+IuZXJ<6N={Hj4P|jB}|bSR>-U zW}Hhj!9^l|6XRT(2^Nd^^^9|gE9eyQTE@9V6HF8FpEAy+nV?<77c*KOuU3~F1-Ygi1{G zNyNu9&Lx%LIuRemIG0j_jUqmTaW0_*Yec+1<6JrkE)ww=#$f49VQwADP2gC2=Y$?>kqpSxCl=8q@iAQ#wccg{vb&B-om=1U(^Tz>q2R=Lc2SubL zOQQ88YTcf^6{@-eALv6V9GVmt1rFKiK}s=(4>ZC)=Hop9Zh197@m`*R0m1Lq0Ct%%y26;kJI|VJNkzv?FauyRD2c zPcK&5jZoSN$?Ll-?m0+5+Dwly#n!w`XlPVx3sQs96cloI?$>7be0WOvKa{VFgvb{O zXAN=({;6M94Mo5mnXk%{T_8P3B>475>PKXKA~De>Y_ib@|I}9jRivEyAl|F4l{OBkHqCbRaJ39zg>Vb-uK4fOB6||}_ z-^hrxN40R$o;Sadx$&4F2C{wyd9u@T2fo(ZkbJb$)gm?Ow#O($8<0+S)R0`+V{5)Z zWrnoL51in+0u*9nYn~?nC};+&xkQ^0i8dt?Z4%LjMWV$;qII1F+UMQP9E!;OBF5^% z0`@KuZtF%+UXh8rkcjrKNc0HE|02;^H+5L_2azK3pN7+)EL?B0TuOi5jP#?OwsZ2~ zZj3daVeixahkKpc4n8*0*C6>BK4fZd*lfW3fa%9DjL=6F;U^0nw$AMk`U`kEpwC{2 zA4?W~zRSbUgFn->7VFO+(zIM_HGXY(?&Ju@_Vms@l()lmz^UoAloG$mskJk@gBvCE zzkO}M?0~sCt^GiqCU5)Tme&q$@#wI|FiNh$--4*jc22WM<@m|*SyABCpSS}5a5a5+ zym&!@W2fUeS7&EU3WiI=yboyFzsge0_s+lDl|Kl^t=7d1a!>k)VJMajA`Zp+ zbse#t(?~&kR0#ZBfuEJm0DWcPxNFzv7rSuF+*X>!ir>t8!j`xUv^w-_6E9Scb-_oi2X1F1+PoKhI zSIex#-+ujl?yI)7XQAlY9^3CY(K@4lersG#er~6&^;3v*wPd(k5?X7znoh^qR)0r` z_qj4#)v+?$vFBXg11`(+Wwx3(v@%=u#E%*@gr|r74PjC1z}o`5cYPAmT8qZz@xdwa}86yYp8Q zTJvJ^0{MFi)Uf^@pXHo|C@GN90!6QplZpu zR_e}YtfaUP6#5ZDP`x9^i(3HS!4a)c^;r}Es92Gl zsNaZ$XX@%f&s3^E73!A-iW9-O!#Sj~!+BP?#vCULjd`PA7RFdlLv&jhyU&-Ao$J5J zP77!E{+C@{?PN#Ao&+c1GYT#X)R3jnI+~g(Lc7O7-)**jJED1QVd_;=z5Ialzp3A% zaE5tg7;bN>Wcc4T*ai-&-N+04-PQDMKileg$eyNeOI?BUlWpsF+Sc!OblINXZ+rRx zT1!{cu9&9XmZqICfxoSK&YzeUIN8)`nc0zV)u!ftU3=UWa3{xi2%HdQgJDfyU(;kcH z%Z4zi>06&|73Xo!^aXMIKBM64Ly_8Kz34uae$-RvkFU$S7S+$G*`975eeY(cwkFTQ z+fBF)P6NHsehc>C>VMSm=*^F!QnyV7d-~KB2$rG#CAQ8=V{wXSWe{Di%7-s))viR_ zWSAKo%I-O^FSr7#6b1GMZ$r2)@5;O@iUMEdEhq|nw}UgPE9UDt{|Us_hB#O220}W) zlEU_J8vt^g&|f=_4zmhBD9wZJsuQJp*dGwO0-qEGIt=}hy35tXHuE%^ThU(5 zJnqVUt^On3A&8Id%5BsozMzsR!Wa19@_XFn2TsAg!$xv;7tq49=efLJw8jky znvEQByd6wMm6H9d?q&Mhjsr*t9z+dABlLFgCEV<=Z${x-k7Smig~(0QkY}ANPj2t$ zc?;YDU39}PS)Oi34ogRU=MEfo1>Uetj!V9@b;h5B*J&?r=C1n8)_gHg-GMIsMhHP> zRqemDH8V$5Th!w(Y(sXr16b-HhoYwMQQD@0qA2$?OWIW|X;x4YC0@V#Q&xWT%8ut8 z2X>^L=Y*;I0@tFMgvb;jTMQ8B4>x-66ulf2LQx>tiQt~B&kbZI&qbe{)SaWUnzIVG zp?F4A;fU-sGVoUr;zGj^z@V)PBdQC=@3EBS*$%>ZeaK>PF;!(Y*Qm;eSe5%A<3%%# zg`GjhDvw@6)0+0W!gE*VpQ|%2nYtIC>o9y9`~=yj+R>uG>EPR;-0V0?z4iqkqry+}=-9v;eLzW+QP$r#a+I}+vf!ZxdZ0#*vepXC z&7-XA1=bDGCdwBhiDN>$t91c-x;DSB&d)8p?RqTj0u4uC=tYt7EFJ!mDlZ!F^L8eHhIyG;nW?DDAr&5gr%n1dbAQ)&Y?>={$Xm%BgxGIDIW`9V6$i# zVx7*!$`Zeq@r8_cIET?KE5D~AwpEXzH0!s6;g0cwl>F-y|GNc$yMz3h+OjseQ6Pao zSWtyEKoO$GF@Z98)8QNk8P3F2&)IJ2gWg z?XmY7uw8%&_M(7Gygw1IAMpkQ4>m(9eS<)h>_a+Wy`YhBjJP3BYqtSkuOg)BhUA5C z>KAdaw8c8-w+x)Rt7XgaSHY6)O*6X9#$V<@*XvO0Z0+^4r1Z;l!BFpH*iyk2%y(?f z$HAUMr9~Vj1>Zsp^(4~3e}Uu~u9CM0Lq;C~OcXWAeI;!Z!>SWNf#PVG^zFbA4TQef zr27dJ3+R691_t4}Z-aNF?mR-$t@|AxOWg+u-LVFP(T=V8k79Yz(0v19#=Lmd0skQV zQ4p=#X1$ z`TeMtKuP5sMPq7FV!!DztYLj`wvGPS9*k zzoN*iAww3I?&*pTFfKSUr(Lbh$!!RmtiQU8(-*9Bi?wvZM4lumz8J1!^+w04ftQIFGH7x6fgt@#qJM+c5| zy<%%W9^?{scNhE&GE8efjRr!N=Z%8Lg5-rP?*`r>5hs4NsPY0}>;cikZz7S?D#kM>+vITw(+YuxYtA-tgvAsf^<+3*bvYxusQ zkHz{|WVWrh^`7(Gz8Lj9CND;b6qx(Umv-MvUP zVo~*Z1VqmapgyCKvC@GX07C`Ej7!E(5(j!|?dJkV)Tn)W9e^Cgvf#amd&8sON{{B> z-qWKV@Q@xo5oUnveUJw_^_>hu223&+ir`s@asD6a&rd-UssACsSiSd2^>ah&Pw7_w zN#MXnYsC`i2@{5zOlXiMth}wK3HKsNn&1sHVbu|7!uJeACL9zGOpM-e59UuA3nFC#KS}r{3jTLy{9_?}T__O+e>K*EL;P28nY>5+-6&`y>c2Ejj(cSN z2PC)`S+b7ZS_5K5(d@Yz&l`Xs+Z{I|(jJ$P5kwWnX!=LYwpc7yJl0Tp0$Ax*=TzVAYQWY*z;X;I*b>A5V-xKtyc#aCw zRG5OW$9&F?mO{x1kZm}NXP<@BeC&rem$0{O7~^Vv zQsBu6pPb(DENqAK>=hl(bHr#Q&@9JRnBkE%+f&Z=*u(N(pEMsMy2z#Uf8D}K-m~w( zbc`B_%dy#3gtDEumnkR~RzT!}`mF-Dx99wqGZiCKbjw&Pkn_0El#jE$Vh%YIIs6FC z_Bg;WP4ph+T#|Fe_D!oT@RSs#kKn===6}u|oV;;llcv9R7_Nq!S{7j}*>b9!Bv@TvQEJEZtSqoRsa=_nkf6;RyYDK44fCWgdW zmAH329QY;Eigu<(N~6Wk0f$_HMw{$jf5i$Rg>eql@>YIF_t{oWLl3Dx^EM>06iCDF zMwDFc0GIrhQp~F4h%jwiNh$JKAd)#(A%uCQ_WC-X-C^fIw-WWs*u8XhjXH^ zZPfwWEgLZ8*FSg*a;6FvEDn1SC6iBof(2g=rWc|!e%r9EoroOd+NS-TSOxSf|+{n@p+Q`x%1YMvK6GJmGG!P&fPx#8cY!Xf5_Q>(f7+^(?Z{q>O(ywf# z<{UB{VVc!{1fVGz44gKGPV$lEO%{}%1428nbUx!Q0`ky=~^{=5FA^m?9rvJ}+(SIXoBK2Pb zm}sD74or?_K`C(ZWe)T+S-{t`z=G^37F=wy;K~RKvXupoG7MQTDa?Yi5W{#!xUzfU z>o6!H_1_OzJ0z%nuAC-D=D_{HQTi7l-N=D=KnDGnt?rov*IT=Dpaaa!Ik4;vnFF6Q z4C(*IDaMQvv>UN9j2}PNb0E8@B`&$gytVRs7VD4Y_;C>;5s>8;za8I8juYAr34?Xw>(lv@&Y!9LA-+C=KWF2ITN6exg9Cel7a(N( zVim@y@Dx1;cz+fD1aTMUc`O)&zopU-sPK7&a{VgrsvT#sC>Rg%@r;fI2y;5IlNob5 zu^~C@6?8G)I&?AI=)@uCeG534!#*8M=ZJ!RI%0f{sT-r5u93qKFy}poMZDL4RuEj~<`b8*4kY`bNBbnuisgg# zh=SQ8$@`^0SY3o=00?2bY_ zb#MB)6=G*lY+6X{SVSq|jbbVyB|cMGudJK~ymlfh^LG%7b})hFvjcOr)%yJ9-jWJC za@{CTS6iQ@t1x$krp>}AOk(j4sPeMFRp)mUrdDO}ZYkD0XndRB3*Q6^$FO9xgkyn#Yav(@kjpHj>L@%1 zlH#z{49~G#x%+Lat^%e$9%5zh-aUg&1&pl}i+_{#vK!FKxpTYZ<095dF>wq&FOeDn z;Kpt7Fln8U`hrSziPU20``li7Pvzyyc#u^)P zG=GMzy+%!;>q(9&^k)pgP)?yaKj|K?e!}qqk#dCO(ENu&)T2B9 zRa8I7i1OCMK*f}CRzq}e{Q->=y@h=nFs0jLvpT^7&Ult#$Yb|}O&QlBruGv=j*~wG zS)?7W045q{mGNmbM669ryVq$nNAdbY5)HX2a=t&Y#MY4+1{Tb1mPl6jT$(i_S^oe4 z=xcIv2_eb}|AcLD!jG_>I$Dug4&tK)Mt8=MI6!AhQNSfvONR0op%bAJ=aOaLh@WAsgH@5d+EZe>Bkx?hcb1rUl6azc9tH7MA z>9K-(VH~agdNy5w4Uyb=%|TjX-gdEv<*#$DUY&DrUu7l+gU5-H#3@%A7Ajq>+r{=j z{ccQw%|PMKgP7OmLA9Lnfj4z3_vlFZPciDaiSZKH*0uSgy%%Q?L*t$1n`{5T8V|D zB5oA2b)-NdwqxM|p%ZRmSx(g47iK2gR<&VBz%HVOXOhE@9e=iir4_?i5JhA?Lbo@Gzf4_mQM*cl3?q&YHCu|7i-|@Vrj_~|L z&o!_++eDVlY|Tv5CuPzCWLsX-=iOO$3HELFo?l=5Kz?0`{QCTVmS1dTwv~nDR~iaa zD36TuJWUv_M$IRYKO$39t6<72nNNI&doTG^&>lUXrUEYVX(R^RBA*<%G4km`+;`_w z4q_^w&O>3B{jbQczR=mouPNeQ=GSGymVYC^mZj4Yx6CiM}77hjMLh?rGbqXT?A%YUNtDJ$j}!2)J5)YL#nF zp=n`g@;Fan#j_s+RuV&FS|}o%Jf5$x_QPOe9b#x;i3qX!D6B0Ct4(3$a-JhKAc$0$S z;2OuL&{4do!YSGt9}`#ZW#4U@f=1bweJv0p=btXYIWt*yv%h`@7%1vmyLy02)$RHO zCEo51oY2dF!;x+S9^&>Fwa)tGPiYh%;%=1>aZlKqXQ1KCYy1@_Q%@qB@dR#}nty(^ z4Gb^@tpuasCd?R&`V=`|c!b(SJg?nA=`Du7Z-G^WzL*dB1g+v3zfDz5C(tuErvq9&Tqb>HWB*jfVl&wrF*9k!R+25xS>N}oZy+C z0k}1kohFstAT|q!yY@}`Q@VC?xU#=%ldc^CM#0VB(X}XV>fE60_JVQW7~^ul7vdbz z#^gricyK1rWj_vhG2rVZ{08KZgy*-`Sc7+hT+NH}TbEjc`@S~CkvWd3HrkD z`vmU{!|xOP3hcY_`v$YZ@cRaRVfcN6`?~RY4sQc2E^6u1fxz!ryJMjE~DV*$VML0~|v)|yoJ{22}dTjjrwv6rj zZiwN;NAxim;%oxGY%;bmC2Znzm%zokEO0{a51~NO87yQfo*rSCi+KoM9AlpG@Q5+G zBFuV{)bTF4P#!-B+gri4WknsVUx8J4(>j-L72$)5{2BRkoPk*hB6|; zLCS>!3tcz)tudpB2vL$0%0iAbmWDYE`?3G6q^I_0g|zoyYu0@GR;hU=I0m1BimKlv>ve!d-Ph0;E9;?s3a8Mq zb&p?2w=g=Lztx~Q+Jww(hO%h9*wCBK;|v5X`#1BOKTb*=P3v(V0TNsQ!9Du77l|7| z-`0cv0n#53^uo2>`^nZKC2}3Mf%K|?tcAL8@fC}Dy7(C+$pYffHM{ug$E1s2Wf&?P zZ(&T+T{t?$2q&L2qL1|?$cy54|JIQI9_ZD7FA5>y{%ey8nEdwuAi*r;qE39hlRvu! z)}G+|s195Od|ZXQRCtip1gn9csrUf2LBt!&_ANAi#rmI}elYEyK$QC@8X2NRtkF+C ziuzf8z0vz^t1OYHoUwgk3_Hdot}e9VNU}QYTv1`4kKORt5wCW`=OYO{A~|x&oK0{8 zZ}hX1qjZO#de|L40ilrtr+Wws>j+x~7G{s3UXVgWi!_f(=&DcL- z{LRW;8v)uCjQ!H}M}5?vL?=0DW*Q$`R}Z@cp?IgpKJHHir04rIigI7a`!wEvgoN1P zg*_S@z+Y_R$60(=>vqv_bE%7MJ@;s^?G{J#QPwZVM4iX-$04NK*HJfI*quQ(z<)#z zGSnbNrID4$ni?Ke<4%Z10Pv6`LWZHpDwNaJU}IYynXBP5_2_V;ct_ zi}Y8azdARIqr>{2g*nyM=clp1>^>l#kFgHh7G}kNc|tsIkFM*I>&&{{4;cRH2}j4jr4 zf6O3!!nv7?%yPo{PCRbA=kk*u7A*C95%LWZ#YMTF+nWC@dS>Hn*+#^~JT%hC7eN#` z9%uv1bWZ$g+85rVcIxYalOGT>=3J8j@1Rb?fU;|P8t@fdqzvFkluQOJeMlN`gdsAq z7(3%_&G)I%z#_IL!F8gmb@s@lfl>572|lLr%x{SjssHQ1QTneyx{(3bN&O$Vx~KkW z-TLz*kS6`NY?1og!}Ko*(?4D5?-J+SJ3zs6?g2o>Id>L~aUeI<*BC_Ex?h74>@y`5 zP)JnJK&Lv|?naC6bgA?1{aK@pNl;sz@8|WtDnItREJ${R<}@hy;^hyb25>gS7nG!t ztCOG{6fjOeF2QQiK^PcWQZ_^9$m(zlEk|`QpLoBXc;VHdQ}+qP$P@2f`dq@ps>6kZ zsOs>stPV*FjofIHOIuyG<_*Zuyv7^3v~>ZnkQ>X@($=U4Wp3Qg5Tz+C;!T;)D7YwP ztKwj(I`Pg1rWJ{F`d8<{J*Z3QQcdj_o38)niT9$Fr{#%vuCMB|!B(Akr<8Mrlw%)8 zoOr+1bmF}k3aAtBAD|DoHgw|s7f4`sMLY3sgE9JB4?tEjG?k6=vxw573}w*|oOmy? zRees>6rf_;)YKZ3JW*ICfEy==QBJ&HOOZ<;Ll&3rsZ$+boI0^{YO=1s3S)}U;~RE$ zVvLZgkk$aeCphBa&IThjQKYg_H)d*fL3Q#!z}kGmUA$FD&UaXm;z1W}J?a(=8bj+v zE5!&Bb>M6L5coyIKN|}|A^gE&C9lW$do6WA4KSaFZy?Ml)Z#pB86ZKvqP55P+Z}~I zrw9Ec(w9Qx$l?E>DCFPVA1VH0K!W#U?FZ*;d7ge8ml;=HA74^oTQyAD#i6xmY4zU` zIVhm{JzIf|NR8|-)&OQYU;o^FMrjD0KfrGVRuCP9Ybfbn}jM{-xAslCl11I!+`G7R+>8=-Q@ieNw((6hfdOyvfkgqhB>?+47h4=9B;LV9RJ z6Fc<#fFwqPC@_72U|4XIw7@^Vrv=|2Nm;Nb+=6ABr3Ife3|TNc%z_NWqRfAz=pRqj zPX^p@Q{s>=lj7gz?=b@D*34OnBm6 zX~L8+6HcIvc4xu|qK-AflR{<e)yc(P#C79FN7y5c0PtU@BBJ&xlV^ zv4|S^kyO?+qkA2Y;7#a$St8#=q^PBDj|J>MqRJp$E@1ybM84J^7RWtUyT_ZY7^bZF zWiQBWX5>F25@|&y2qUeq6BgF4Bmfe8>ts*zuQv50f1j{0@|OV#UJNHhN`E>E`Q{$v z>qx!_{#YG-C$)8w?*fgY_WfCWfUD+JL)938OTG{36kHWX@w3tm2^>wxK#035BYE_5b&%^4@jtPXQ+|u>m z(~@dy8lYmr)8QPiJ!4NJigAvhvz;VriFgF=#R3Qzj|Uj96tf$7%{a$4b61_Pt-2EV zrT-S}cZ@R&T&@JZ2V{)dAf~gO2;91|RUk^SO5A>+8RG0rR>g|q#z5F8J% zemCx1vhd)EjvHR*Lu~!SyIEH|&^O;sYXY(Q0YFUUM?U_?D@NUT8j}MO>X3$zmr;;@ z0$LWN;j{(^Pg0QHeMm(f86jPg-0v;EyBiX>!ficmB$f8L1OtvAq$nL zeRd!KmLp*p8r!jDgfD~Qjs&WXmqLLBHVrqO#E9z(=C%{uRl%aLaZ1>86K!x$RqYYt`b#c)l5%Qh8=MO}AVKqUvlU#9mHIstQ>kVZ6v zd5=Y#bAX^G5v3H4#g7JLE==GvZM0RN7fKV%n_yf}UyZ`~z5~+`_V``WfTiSxsj8?~ zwBhtO1T?EL1I*9?i2ginE%l7BqX96(j_r`q!;Y(fpzPR49lGtfNZK(eiXA*YrR?ZL zLuRrgMcJ{5q1ldmBkgGE)sDT?Gs2FofQ9zWe0gUSLv9fmVpjJW(oKeJhpUkx*Uj#k zA%BLA%8=A>LppE zRQ<@3w%k@AZE>}PpYM4G$+CX%fOL2LxNW_(rk`MJ)Q@k4DMtNZ!HQ}}CY6k^!!8(w z&MB3Ee-t}TQEaH5&tOd1aTMN0o;;Ty-433wmv&4Kv*Wotq#b#Jv0+EzDT*=d_};LC z1GzOoj5g0(jTvod>b3$AwI66DK%J-9usW?heU|oPlKu9?_;2qnHvljf)2) zcp551+y8Vs?Tg0e08_&F>;xpp*Pukh~Nb>iQ5V=_W5@BIZc$VY`AzdfFewaUR^5+15h_CO*59hIo zXvP`NM9AGx?2{M|sIWx9_XTfP@vSO+PQdpBPoniC{t*=(Qt4l-a5%antf=;ubj*sYaCH{Oqp!dSbeOi|*?XOu-WLkeLR{(yv=)I_}jd9yh#QfAlPvPcck8ih4`+Y@!O)anT`)aF7TurAEm(6lDeUjLo3RCs3 zAdeP;MR7ci9Jn2yxE=rL>i2wo|Hg*I3S0AcLd6D)J8;T18DC61mAHHt$5+A)d2N_c zXxsX3#%Tt0W*_Ur#fV5Wb$Al>k*Kq(snf5AsNib=iFVsWFGvLw{UH1w5tpKE8wm|v zn7SwGen=257a!sfuP#;xEiNHPjLqQ+`9Th+@j;Fp^+68t1t@jOLi#;dw>6Ik9d?~! zPUFh`*4F$BL}^?izX2h?_JGg&*_y?Up8#Ehhf7W|4S|MQM_**2beMTx^jz8scp^sQ zw&vfXO{PG*81Org7W}nnrkxnhJYArzK@waLv~C2fO%YxG#_=5U=y;ka*7Cb6?MRd?6uZzrO?ZV^zU<(a;8f9@!U!e*`9&JG{DZX|I zjH7?#3mU%@oy5MNjeR!alMssX8aQtHdKLTMmcFjm!o;H7o&4_9a-7-ebm5a=!Szsy zdWT_i;K1*HamQwR;wGnB>{uMPx zsviler6H+zy2nmmea2xpBU<}2&8Bs@o}5Z;Cj%}KjL#*EjQ-4T4%X9 zm&Df*aKjnmJYcw5evyQf6sF+2V09@!7S5e6h#SFE`*E0qQ4bd61zyK@y}k$IFI`Qi zZOg~HR-LHFQ``c0?K+gKB(cE{FLf65NNjMq0$;oEp`V6y{iR z|Dnf2fV*XiJK&9<)sa6DG;nIzu?g`Kx`ylV)fcQ;{i}B7T)sI?+IXQJ#EQJDt$%PzQX56h7?WSUzEEGkFnWqqXrhi<2M&DV!>{w z{Jat%p_CIivdv=k0=S&q(WjZ1|vTT>q*=6-3r`6ArRY0Vri zrSnCD?!YGyt$%nkOagh(0aE0nL@0w>fFRY=cLFCkNT#>5DKBd3ve{PeWdm-M-?`|s z))a!Tt949v5hnLHv43>q8+d{DSZ8c&W{J4yMDRWicVJ+$elr-OA9lA4#A*qF>`RIQ zyZvb4F+mhxeBpQU(A}Vdxn&Pz5T6HPL7+$nqD$C~;^)ujSm5HK))~sz?~58iW5^+i zeKwk3;d1Ek!D`6{JBij|vo)m!Z(|T6CDZyZhrls1G6w9u+Xz9-yAA=^NxuT~&_oL8ib~ zcwFDT3b1ve9>oL)id;c|f&QBW1x8#(s}Pm+%vb;I-Fdnl8VRBNN-op)l|Wyrs&9pOytaxun%U6nEzx67!qC#z{3iO@ z;IrVN_#J(Xf|R}O?1U*-D6$o?9lzLwhS3fSWy3HWcN{ILZ*Qad+5_*Oxz2dtj0etm z;QvhzSj023Zi)Rby67VN0(@7>KEJ*a8^u@H0qCz0QfIHNue!jl?rTcw>U^c4Xjw^l zg^13t_j>VG343jc--pzR{k6p&`9oxircYRLr`c;wQIAhYb)`=Q~NSsZFw19*TpZN+RLhI?Nt|P#d-7R7v%TXjD3sP z?Pz?bN1L5jV-UNpZKf?bE{QIl9=eWvO zQj186zq-O#TT(3b?kky6Rj% z-+Jy{YNETW##c4B*gn%&z~DY|t2g1k#- z6?hh0U5s)2W%CzYnm4xqpN}kCTucEr95<5LLuKiR*foR^6 zs;X+gU1piR&R=@L1$tM~)*m zJuTglo}QkOo|&GNo}E5DJtsXkBQ3*`k)Dx}k(rT|k)1I;BPSy_GcD7RnVy-EnVFfD znVmU3Gbb}QD=o{Bm7bN6m6?^5m7O&`D<>;AJ1yIhot~YMotd4Lot-^BJ10ALdfIfy z^z`W&(=(@MP0yY_eR|IH+?=!=M^1W9MowllW}W@ukSK`YOMCFP+$AU*fk{_)5?!6jjyn@P*IrUs2<;SC(JttF04R zE2DK(8%4?ZHZdxl&s#4uzr50C;>dTy<2`?T^PP|Yv31o|C8k;=R5j{{@nqSoFJGKz zj+?mG`A}y0Jt-4E!6Ib8V#s&#hm3RB{U&^0G}S}&QB&=UF)-zIBeGRov#|15Qd~no+0_WHGe5Lg@AytjI*=I^Mv@_o7f-*FA<*5HF6bYLNUxOE1Q1yJ(CjPZ-O6HaT zie^N-&~R3DEvr%~{(0(R4ffNh{GBE8ccyQt{2GC;p@d&OKtWUAUO+1(+a8lYM2V5* z@dA5YcWYONEDxz{q{C?0;)SK!(~NYXEZRL0Pw(bu#CxGnH_{DyI@$NqOL?qQ{78em zj$bUptDWCH^q&XjE-vO5IfV}x8d#Xsh3cYZbnJyQkT40bq!nFVjkrH~Jwo_x`Y*b= zK18?x;X+i{6$oEJcn`uHtfS7jp{r{n!oMI)#Cogymt9?J5KjM9SJ!cbpCT;9>iC)) zySfe|w5{ywS{kouOA%@a4s?(* zke~8dSJxKE%R#794}@IzZulJOkiQ?H8zH}$%Bl9!!}V0nC_ zcJZhS&&x<2PejsI;J@K$R~K9$t`z(>;=dGrbdxqOA#rs~UP4k+Y+-^uc9NxkLJ~lE z3GsykNdE}_U7#O_kXI7^cjCVkuz{q{Pe_c_ED3Ol(ETvNg`akH%|uAvO)=H|6XJ^p zfT7UmJAj#YJb}%N`fv-6A2c@;Ff$=>ZA^Yb((2g!1bb85kEK2{6XIP179pAC?htS- zlszXQ@zxlaeoO4k1bb`T%!HJ+*1UwY)qV04a+>-UCOBhU{S$Hk$xBE9#>|8ya0jb` z0Wq$G_&psjzVx@3T?x*nzN`DJwYJ9H5_@Zm3v4%mcQf*{Kgx&ZrVLQ_kda3jmn1BT z`E67gg#&~?z(KQIU+fk+-8XjAIHwkC9)lJF*Dw6w0%VT#Nr2|S!2c1{fi&0>cTn=Zg?6;Y z79`l$#uX)`tfsG;`o#7}o~OXsA@}5A&$b=(JMljUGR7i|`;an#zS_Drt~K@+%5mfW zL;P>S3_Tx!xKE{=AM{so>;cHj$&ZpLcJSgentv_p>Y9lAxGu^pHfN7k3^?n3Ggf|f z)3&}_h0n|~3s?>yV09mnNBIejme@tng(8=vt&f1$y^za#7dHw#L1Y%}1pF1?97cPx zdk}EOOS!Evg$YS(nOCdhP|BLDg$a!@u~YiOrGO#Rj1mWW<{Q7X?SY)%<38>SAt$EE zx;k!cY-@}YuuP=8?&|7#o-$7LDr09%>{-^{WU#&202#lajJ=P;mYpFP^xp=gKZ!l* zPRe)@>Co|3+7Z2s7bdKWi(S#Dw^AFD%yn^Q|4}X)3hjoiU0tUJ z0>=lO|59#j>~?rEN)668@~sp+w?pnx$~`9J#+v0i0bj$q_1CVh6MYb#lyalw+t!%a zJk+$Xd=uq^`Sv1sT#UN#6y;p?1nk=y=F?A+z6pJX6KkV!9;E-5w#CFw?ibazP?>b1 z{b_{Ejg*=FBz+oVwv9e51$?uXo)S-<)} z)gv$2f9-`Y7oqR3!9Dwcn57BvY!BF{?myDibs@sIvp_5KiD*B&>!vGVoh4>&)b=26 zfSs(->D|-N)wKZkaZaI2SiPh!#psvwq03dc7rI<3x+4v}#HbOVzh?HazS@hHJt(4Y zTdiOB)jsY6C{p7_CyTKES%`mb8Mh@yK$B#u?Lvf)#E#n@r~M^P5!h9d8{xK?aX9+% zRIExGC&L#k=isJh_7|hH^)c4XqqQwD9hTpX)(*x!VL3HQ`%9l&EDw*?Zi}}9_M3iB zS#BS#eb@ix7{p#ks7A`mwkbe+Eb&&$pGRxQ1}}PMwAMA`Hp{O^YdeOnx7;*Z`*PR> zq}(@r0#aTZJ^{$Lj+g?-o)Htk>&p=nfO=oj1Yo|GGzB>KjGO}adn2a+e$S{WfWJJ- ziu>0`S#kgOQC6hBJIea-XzhbhR^aQStl;+9C@VPAM`vFme6-Ip9(Jv=xPkbm7?SRd zALJZcb!!V{cat0rv*-YdH9|5QBxA_dLn58Kp!O0QrHCu>;mMTQCWb% zBJhJn;0KF{DJ+FW;0lW%fff}{5f-hrOkz&C*NsApXl;YjW2{?7X{TcKJ|{Zw zm;(4?NmBqnmNW(M$3{*8{JoJ@F#K?&75BlBR=__WX(jDQEAXkqd+8)sLC^Rql5OX?Mg}4-P`1wzdz_w#R%F^S43Tu|9WLJ{qJw7T;?5e314+ zzX^ym^*;yqJNi$-eOLb}ydQAX(gCbNRz#j0WJSWZK~_L$`DIef0m}r)`Q_}kv$fyE zSR2pQ9*8@4>)F^R@SJ7K+1l^=K4sZ(w$>GYn`O<}qQq@FTl?)mE0P`>^ps`Y+1f3( zmtzonZOC{aG@Ugb=_pdT?>cJ=?%Rh>0sUjcZnK;ms~sDDheaQYe7ehWe5`hKw?;Q09H@j!Za^myR>X3P{o4vv`u$em-e0C|3_6__uNwSwyPu~wx1 zeXN!EW33SP!B{Ir!@Pz0B4ax(CGJh&9An)uR=Yd)+;wBM_hYSV#%f>29*8-Dpzpbj zW3_GZ*0!RI~-l4h;c^UCKuWp|1Y%P8Wy;cll0 z`RWp0n_hB?aJ2Z9>#@8YrQ%yvI@cF?#mhkZ>U38%Z+FHz1;iLK9qB9Mn0kvVK2FB@ z+=3TBgT-rr48&S3Z~6Wi^QFwz^p~9ijCrxR+SCo_)VwgQ7S{+BE=q8UI|Hr{l5s=t z28G8L((vM1F)!>`687i=og$9aP7zn?YFHW<@%1WQApL*%)Ao|oGg5_YmEW^fc%=%< zRJcrqt5tZH3LjA6(<@!ZvAFjelD$G{lY!zOq!ZH;uQ{ieA-lf6^ zRQR+CJ5+d3g&(Q#gbHbQ;jCE~*i#BGy<)Q6;YiI&O|#>vaV8$>=Gs%{;dz=14+3Sv z)U3&PrQQXZHIDS`Ty5T!7hvau*pjfs>y4z(hKvzqqvCN3NA>~%ID)JOowvpA%H+3M%_!EDiVf_r@eTVIJFi#1-u4P z1WXb~09k($>2x$qg?Y)Aj1q_DRBcz83ZvoFt285whNG!6HC%t%A#N_i^hI}SL_=Dq}jB0!~!7ER}^Cvh( zoX-<@)hPH51?QSSuiF(IXH`V}sQASjJrw-ni4x8=bYA?VIOYCJ!MV=OD7aun=Kye3%%Ge}?F=2W#;UIt9J)Gh6Tr5x38lUa)da4&b`+I>eM0psDa+R;jm7(X` zz$cua`{iX9znazu1H~cmTaBtOH3~jJz*WQ)WC4CJ{IF@WQ$U|nX_dX;Hv&F5CQ*x5 zepBdLtAO|P|0cmFLECwq9@X;}mhR4*+*KcNeuh98&mc>m@$2NnBs{ff3!AOKY#Y!KEt!Wd0PyaWn_GhL_n4$7( zXPQ$$xCesQEr3UBfB0Hi=I^>GPJw4^%Gsy*N9Icx;M6CweH$~@Vc)TK05%n3ykoezbQVADjyXaP45Li1_;#ic!b|32{^`k zE1g0BpW%zkTkChER%;#)<~$yM$r6pbyFH7Ioz1DO?nz;TE-sB(6;CDd2=qE z@1O4RcpDnH_1C?{IjRmehLuwI${Lx*~7E{1W^W6r_5v`O;HXm6}G*xk8|^ zk=BEKi5_qFM%885BVAESymV;Lz3cJ+SO@HWuJhN_R@X=^{_k=Bzh{8-%l~T5C}(G6 zicB#!dv+HGPhBOpkBU9zp`6Lc7RcQ@Pdy6tQ{h{H3MDM6 z^PmV9pe)b#yK_9A@@h`(HB`Vle|=dQX7Q!ITHg}-3v~vyprRVz0Y%L$t@bRbs9s!B z;VH!i|2j`eeS=2#Rrqjfg2gu)hNleNJteiZB`Z8QNl?2&E2}N3^m$5gd}IYkOfe7j z2t%zdE32r+hgpE54ZR8=o8SY{?jb>vE#|7nxsNayhu*7(|bufL$ut9g8-CH@lFqD=La)mM4z;3>2< zMVH}JOlf(Q2b<8@^q?VwKoqm;<(|bTX{Z6!-Y~_ag}D$?URCF-^%pf16wI4ny}%Qu zJ(cgh^dqrnS&dqgG?-E0tCGAu>8a_dSz4v9(py;*28t+xj_KZdY`jMu5hc6|g+Z0z z0e}Ktk@X-Y?}j54ny45^swiK~(@!O}ehhe@bhGoFmKNMOg0Nu%PXbQp$4I}qIg-Q6AeV(%p#8? zH6!)ktnvBj1L&h5)t7S5j%z;3&fvRB68>Lk@Bj zb`jdRG*7|&Y)|o=`5B%Hvo%t6b1pbC-~y2iLXGg!n(A=SujXFEkAFpeKlHuUWbVPUUAHKpoDlli|gu?orRapEe;J`oFbMf28V3x%B$EdRRd1oE~!Dk%Vn_pj9E)|ciw&=^k@|Q&;!Y@6?g_@RHx1th#3POLa43`>r;&oOv zTB>-6w4|y&l`{h*efime7^ zS!AlnFm!en8B|Y>n zK{}R7LiQWa`+1Iy*CEA+Hd^p+*l#>{F9(b$hJ0iHz!4JQGWH9|SWo}|90`0@Z_pdh z=htJMgV#J6Xomg3sHY#Es|w|4$oN&(dHFgfmG+1P7-s4aLDp+yx0vU4)K~ zj{OUYgFirL1(%A0UBrTmAY#=;7Zn^`T!aoTx(nWuyC;UL2XF2p_vDhic{}8iVxJ@v zIxU3mU`XsgY@mI4U@!RDB%0j8vke!XH(-3?IsOCj1%3@6A)a5@w0#zycK{w3k3SY0 zy2$p+K4=u95YKg}Ho1O~&#Lh;rtBJHX5k%`70sDUekyv65ACELZDreA%2^Ue{Y-=! z&;9b9BlWOpr2EABy)ZoIpZSr{S4hLw5gUjUj9&>yeBzlmYG7bOxXV|QE$d=>6sg8{ z1}WEd2I|st@%jF=HiTCZYCNvjfrjUqg+nF+RL#C7X7(W^mWq95B@frXRxdx_j8Fb0 zZrIul+mp67+ehW8Uj9QnM*zoZ;%JkpTi|j3rEFRJFIsV$0?*=a2@02|^?lKE|bX0ZX`$ILq9dpTSe6QNB sOT5M(RL?-o~R9bOA0IoZdoB1Dq@}~H82XJcm1r4)zL;wH) literal 0 HcmV?d00001 diff --git a/typescript/batch-ecr-openmp/src/openmp/test_benchmark.cpp b/typescript/batch-ecr-openmp/src/openmp/test_benchmark.cpp new file mode 100644 index 000000000..191ebef02 --- /dev/null +++ b/typescript/batch-ecr-openmp/src/openmp/test_benchmark.cpp @@ -0,0 +1,438 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +// Simple test framework +class SimpleTest { +private: + static int tests_run; + static int tests_passed; + static std::string current_test_name; + +public: + static void start_test(const std::string& name) { + current_test_name = name; + tests_run++; + std::cout << "Running test: " << name << " ... "; + } + + static void assert_equal(long long expected, long long actual, const std::string& message = "") { + if (expected != actual) { + std::cout << "FAILED\n"; + std::cout << " Expected: " << expected << ", Got: " << actual; + if (!message.empty()) std::cout << " (" << message << ")"; + std::cout << std::endl; + return; + } + } + + static void assert_equal(int expected, int actual, const std::string& message = "") { + if (expected != actual) { + std::cout << "FAILED\n"; + std::cout << " Expected: " << expected << ", Got: " << actual; + if (!message.empty()) std::cout << " (" << message << ")"; + std::cout << std::endl; + return; + } + } + + static void assert_equal(const std::string& expected, const std::string& actual, const std::string& message = "") { + if (expected != actual) { + std::cout << "FAILED\n"; + std::cout << " Expected: \"" << expected << "\", Got: \"" << actual << "\""; + if (!message.empty()) std::cout << " (" << message << ")"; + std::cout << std::endl; + return; + } + } + + static void assert_true(bool condition, const std::string& message = "") { + if (!condition) { + std::cout << "FAILED\n"; + std::cout << " Expected condition to be true"; + if (!message.empty()) std::cout << " (" << message << ")"; + std::cout << std::endl; + return; + } + } + + static void assert_greater_than(double value, double threshold, const std::string& message = "") { + if (value <= threshold) { + std::cout << "FAILED\n"; + std::cout << " Expected " << value << " > " << threshold; + if (!message.empty()) std::cout << " (" << message << ")"; + std::cout << std::endl; + return; + } + } + + static void assert_less_than(double value, double threshold, const std::string& message = "") { + if (value >= threshold) { + std::cout << "FAILED\n"; + std::cout << " Expected " << value << " < " << threshold; + if (!message.empty()) std::cout << " (" << message << ")"; + std::cout << std::endl; + return; + } + } + + static void assert_near(double expected, double actual, double tolerance, const std::string& message = "") { + if (std::abs(expected - actual) > tolerance) { + std::cout << "FAILED\n"; + std::cout << " Expected " << actual << " to be near " << expected << " (tolerance: " << tolerance << ")"; + if (!message.empty()) std::cout << " (" << message << ")"; + std::cout << std::endl; + return; + } + } + + static void pass_test() { + std::cout << "PASSED\n"; + tests_passed++; + } + + static void print_summary() { + std::cout << "\n=== Test Summary ===\n"; + std::cout << "Tests run: " << tests_run << "\n"; + std::cout << "Tests passed: " << tests_passed << "\n"; + std::cout << "Tests failed: " << (tests_run - tests_passed) << "\n"; + std::cout << "Success rate: " << (tests_run > 0 ? (tests_passed * 100 / tests_run) : 0) << "%\n"; + } + + static bool all_passed() { + return tests_run == tests_passed; + } +}; + +// Static member definitions +int SimpleTest::tests_run = 0; +int SimpleTest::tests_passed = 0; +std::string SimpleTest::current_test_name = ""; + +// Include the structures we want to test +struct BenchmarkResult { + long long sum; + double execution_time; + int thread_count; + int problem_size; + std::string instance_type; + std::string version_type; +}; + +struct ComparisonResult { + BenchmarkResult sequential; + BenchmarkResult parallel; + double speedup; + double efficiency; +}; + +// Function to get instance type (same logic as main.cpp) +std::string getInstanceType() { + std::string instance_type = "local"; + const char* instance_env = std::getenv("AWS_BATCH_JOB_INSTANCE_TYPE"); + if (instance_env) { + instance_type = instance_env; + } else { + // Try to get more specific local system information + const char* hostname_env = std::getenv("HOSTNAME"); + const char* user_env = std::getenv("USER"); + + if (hostname_env && user_env) { + std::string hostname = hostname_env; + if (hostname.find(".compute.internal") != std::string::npos) { + instance_type = "aws-development"; + } else { + instance_type = "local-development"; + } + } else if (hostname_env) { + instance_type = "local-" + std::string(hostname_env); + } else { + instance_type = "local-development"; + } + } + return instance_type; +} + +// Sequential benchmark function (extracted from main.cpp) +BenchmarkResult runSequentialBenchmark(int problem_size, const std::string& instance_type) { + std::vector data(problem_size); + long long sum_sequential = 0; + + auto start_time = std::chrono::high_resolution_clock::now(); + + // Sequential version - simple for loop + for (int i = 0; i < problem_size; ++i) { + data[i] = i * 2; // Simple computation + sum_sequential += data[i]; + } + + auto end_time = std::chrono::high_resolution_clock::now(); + std::chrono::duration duration = end_time - start_time; + + BenchmarkResult result; + result.sum = sum_sequential; + result.execution_time = duration.count(); + result.thread_count = 1; + result.problem_size = problem_size; + result.instance_type = instance_type; + result.version_type = "sequential"; + + return result; +} + +// Parallel benchmark function (extracted from main.cpp) +BenchmarkResult runParallelBenchmark(int problem_size, const std::string& instance_type) { + std::vector data(problem_size); + long long sum_parallel = 0; + + auto start_time = std::chrono::high_resolution_clock::now(); + + // OpenMP parallel for directive + #pragma omp parallel for reduction(+:sum_parallel) + for (int i = 0; i < problem_size; ++i) { + data[i] = i * 2; // Simple computation + sum_parallel += data[i]; + } + + auto end_time = std::chrono::high_resolution_clock::now(); + std::chrono::duration duration = end_time - start_time; + + BenchmarkResult result; + result.sum = sum_parallel; + result.execution_time = duration.count(); + result.thread_count = omp_get_max_threads(); + result.problem_size = problem_size; + result.instance_type = instance_type; + result.version_type = "parallel"; + + return result; +} + +// Helper function to calculate expected sum +long long calculateExpectedSum(int problem_size) { + // Sum of i*2 for i from 0 to problem_size-1 + // = 2 * sum(i) from 0 to n-1 + // = 2 * (n-1)*n/2 = n*(n-1) + return static_cast(problem_size) * (problem_size - 1); +} + +// Test functions +void test_sequential_benchmark_small_size() { + SimpleTest::start_test("Sequential Benchmark Small Size"); + + std::string test_instance = "test_instance"; + int problem_size = 1000; + + BenchmarkResult result = runSequentialBenchmark(problem_size, test_instance); + + // Verify basic properties + SimpleTest::assert_equal(problem_size, result.problem_size, "Problem size mismatch"); + SimpleTest::assert_equal(1, result.thread_count, "Thread count should be 1"); + SimpleTest::assert_equal(test_instance, result.instance_type, "Instance type mismatch"); + SimpleTest::assert_equal(std::string("sequential"), result.version_type, "Version type mismatch"); + + // Verify sum calculation + long long expected_sum = calculateExpectedSum(problem_size); + SimpleTest::assert_equal(expected_sum, result.sum, "Sum calculation incorrect"); + + // Verify execution time is positive and reasonable + SimpleTest::assert_greater_than(result.execution_time, 0.0, "Execution time should be positive"); + SimpleTest::assert_less_than(result.execution_time, 1.0, "Execution time should be reasonable"); + + SimpleTest::pass_test(); +} + +void test_parallel_benchmark_small_size() { + SimpleTest::start_test("Parallel Benchmark Small Size"); + + std::string test_instance = "test_instance"; + int problem_size = 1000; + + BenchmarkResult result = runParallelBenchmark(problem_size, test_instance); + + // Verify basic properties + SimpleTest::assert_equal(problem_size, result.problem_size, "Problem size mismatch"); + SimpleTest::assert_true(result.thread_count >= 1, "Thread count should be at least 1"); + SimpleTest::assert_equal(test_instance, result.instance_type, "Instance type mismatch"); + SimpleTest::assert_equal(std::string("parallel"), result.version_type, "Version type mismatch"); + + // Verify sum calculation + long long expected_sum = calculateExpectedSum(problem_size); + SimpleTest::assert_equal(expected_sum, result.sum, "Sum calculation incorrect"); + + // Verify execution time is positive and reasonable + SimpleTest::assert_greater_than(result.execution_time, 0.0, "Execution time should be positive"); + SimpleTest::assert_less_than(result.execution_time, 1.0, "Execution time should be reasonable"); + + SimpleTest::pass_test(); +} + +void test_sequential_parallel_consistency() { + SimpleTest::start_test("Sequential Parallel Consistency"); + + std::string test_instance = "test_instance"; + int problem_size = 100000; + + BenchmarkResult sequential = runSequentialBenchmark(problem_size, test_instance); + BenchmarkResult parallel = runParallelBenchmark(problem_size, test_instance); + + // Both should produce the same sum + SimpleTest::assert_equal(sequential.sum, parallel.sum, "Results should match"); + + // Both should have the same problem size + SimpleTest::assert_equal(sequential.problem_size, parallel.problem_size, "Problem sizes should match"); + SimpleTest::assert_equal(problem_size, parallel.problem_size, "Problem size should be set correctly"); + + // Thread counts should be different + SimpleTest::assert_equal(1, sequential.thread_count, "Sequential should use 1 thread"); + SimpleTest::assert_true(parallel.thread_count >= 1, "Parallel should use at least 1 thread"); + + // Both should have positive execution times + SimpleTest::assert_greater_than(sequential.execution_time, 0.0, "Sequential execution time should be positive"); + SimpleTest::assert_greater_than(parallel.execution_time, 0.0, "Parallel execution time should be positive"); + + SimpleTest::pass_test(); +} + +void test_comparison_metrics() { + SimpleTest::start_test("Comparison Metrics"); + + std::string test_instance = "test_instance"; + int problem_size = 100000; + + BenchmarkResult sequential = runSequentialBenchmark(problem_size, test_instance); + BenchmarkResult parallel = runParallelBenchmark(problem_size, test_instance); + + // Calculate comparison metrics + ComparisonResult comparison; + comparison.sequential = sequential; + comparison.parallel = parallel; + comparison.speedup = sequential.execution_time / parallel.execution_time; + comparison.efficiency = (comparison.speedup / parallel.thread_count) * 100.0; + + // Verify speedup calculation + SimpleTest::assert_greater_than(comparison.speedup, 0.0, "Speedup should be positive"); + double expected_speedup = sequential.execution_time / parallel.execution_time; + SimpleTest::assert_near(expected_speedup, comparison.speedup, 0.001, "Speedup calculation should be correct"); + + // Verify efficiency calculation + SimpleTest::assert_greater_than(comparison.efficiency, 0.0, "Efficiency should be positive"); + SimpleTest::assert_true(comparison.efficiency <= 100.0 * parallel.thread_count, "Efficiency shouldn't exceed theoretical maximum"); + + // Verify results match + SimpleTest::assert_equal(comparison.sequential.sum, comparison.parallel.sum, "Comparison results should match"); + + SimpleTest::pass_test(); +} + +void test_edge_cases() { + SimpleTest::start_test("Edge Cases"); + + std::string test_instance = "test_instance"; + + // Test with size 1 + BenchmarkResult seq_tiny = runSequentialBenchmark(1, test_instance); + BenchmarkResult par_tiny = runParallelBenchmark(1, test_instance); + + SimpleTest::assert_equal(0LL, seq_tiny.sum, "Size 1: sequential sum should be 0"); + SimpleTest::assert_equal(0LL, par_tiny.sum, "Size 1: parallel sum should be 0"); + SimpleTest::assert_equal(seq_tiny.sum, par_tiny.sum, "Size 1: results should match"); + + // Test with size 2 + BenchmarkResult seq_small = runSequentialBenchmark(2, test_instance); + BenchmarkResult par_small = runParallelBenchmark(2, test_instance); + + SimpleTest::assert_equal(2LL, seq_small.sum, "Size 2: sequential sum should be 2"); + SimpleTest::assert_equal(2LL, par_small.sum, "Size 2: parallel sum should be 2"); + SimpleTest::assert_equal(seq_small.sum, par_small.sum, "Size 2: results should match"); + + SimpleTest::pass_test(); +} + +void test_mathematical_correctness() { + SimpleTest::start_test("Mathematical Correctness"); + + std::string test_instance = "test_instance"; + std::vector test_sizes = {10, 100, 1000, 5000}; + + for (int size : test_sizes) { + BenchmarkResult sequential = runSequentialBenchmark(size, test_instance); + BenchmarkResult parallel = runParallelBenchmark(size, test_instance); + + // Calculate expected sum manually + long long expected = calculateExpectedSum(size); + + // Both should match expected value + SimpleTest::assert_equal(expected, sequential.sum, "Sequential failed for size " + std::to_string(size)); + SimpleTest::assert_equal(expected, parallel.sum, "Parallel failed for size " + std::to_string(size)); + SimpleTest::assert_equal(sequential.sum, parallel.sum, "Results don't match for size " + std::to_string(size)); + } + + SimpleTest::pass_test(); +} + +void test_thread_count_behavior() { + SimpleTest::start_test("Thread Count Behavior"); + + std::string test_instance = "test_instance"; + int problem_size = 100000; + + BenchmarkResult sequential = runSequentialBenchmark(problem_size, test_instance); + BenchmarkResult parallel = runParallelBenchmark(problem_size, test_instance); + + // Sequential should always use 1 thread + SimpleTest::assert_equal(1, sequential.thread_count, "Sequential should use exactly 1 thread"); + + // Parallel should use at least 1 thread, likely more if available + SimpleTest::assert_true(parallel.thread_count >= 1, "Parallel should use at least 1 thread"); + + // Thread count should match OpenMP's max threads + SimpleTest::assert_equal(omp_get_max_threads(), parallel.thread_count, "Thread count should match OpenMP max threads"); + + SimpleTest::pass_test(); +} + +void test_expected_sum_calculation() { + SimpleTest::start_test("Expected Sum Calculation"); + + // Test the helper function + SimpleTest::assert_equal(0LL, calculateExpectedSum(1), "Size 1 expected sum"); + SimpleTest::assert_equal(2LL, calculateExpectedSum(2), "Size 2 expected sum"); + SimpleTest::assert_equal(6LL, calculateExpectedSum(3), "Size 3 expected sum"); + SimpleTest::assert_equal(12LL, calculateExpectedSum(4), "Size 4 expected sum"); + SimpleTest::assert_equal(20LL, calculateExpectedSum(5), "Size 5 expected sum"); + + // Verify the mathematical formula: n*(n-1) for sum of i*2 from 0 to n-1 + for (int n = 1; n <= 100; n++) { + long long expected = static_cast(n) * (n - 1); + SimpleTest::assert_equal(expected, calculateExpectedSum(n), "Formula verification for n=" + std::to_string(n)); + } + + SimpleTest::pass_test(); +} + +// Main test runner +int main() { + std::cout << "OpenMP Benchmark Unit Tests\n"; + std::cout << "============================\n\n"; + + // Run all tests + test_expected_sum_calculation(); + test_sequential_benchmark_small_size(); + test_parallel_benchmark_small_size(); + test_sequential_parallel_consistency(); + test_comparison_metrics(); + test_edge_cases(); + test_mathematical_correctness(); + test_thread_count_behavior(); + + // Print summary + SimpleTest::print_summary(); + + return SimpleTest::all_passed() ? 0 : 1; +} diff --git a/typescript/batch-ecr-openmp/test/aws-batch-openmp-benchmark.test.ts b/typescript/batch-ecr-openmp/test/aws-batch-openmp-benchmark.test.ts new file mode 100644 index 000000000..daff06f15 --- /dev/null +++ b/typescript/batch-ecr-openmp/test/aws-batch-openmp-benchmark.test.ts @@ -0,0 +1,355 @@ +import * as cdk from 'aws-cdk-lib'; +import { Template } from 'aws-cdk-lib/assertions'; +import * as AwsBatchOpenmpBenchmark from '../lib/aws-batch-openmp-benchmark-stack'; + +describe('AWS Batch OpenMP Benchmark Stack', () => { + let app: cdk.App; + let stack: AwsBatchOpenmpBenchmark.AwsBatchOpenmpBenchmarkStack; + let template: Template; + + beforeEach(() => { + app = new cdk.App(); + stack = new AwsBatchOpenmpBenchmark.AwsBatchOpenmpBenchmarkStack(app, 'TestStack', { + env: { account: '123456789012', region: 'us-west-2' } + }); + template = Template.fromStack(stack); + }); + + describe('ECR Repository', () => { + test('creates ECR repository with correct configuration', () => { + template.hasResourceProperties('AWS::ECR::Repository', { + RepositoryName: 'openmp-benchmark', + LifecyclePolicy: { + LifecyclePolicyText: JSON.stringify({ + rules: [{ + rulePriority: 1, + description: 'Keep only 10 most recent images', + selection: { + tagStatus: 'any', + countType: 'imageCountMoreThan', + countNumber: 10 + }, + action: { + type: 'expire' + } + }] + }) + } + }); + }); + }); + + + + describe('VPC Configuration', () => { + test('creates VPC with correct configuration', () => { + template.hasResourceProperties('AWS::EC2::VPC', { + CidrBlock: '10.0.0.0/16', + EnableDnsHostnames: true, + EnableDnsSupport: true + }); + }); + + test('creates public and private subnets', () => { + // Public subnets + template.hasResourceProperties('AWS::EC2::Subnet', { + MapPublicIpOnLaunch: true, + CidrBlock: '10.0.0.0/24' + }); + + // Private subnets + template.hasResourceProperties('AWS::EC2::Subnet', { + MapPublicIpOnLaunch: false, + CidrBlock: '10.0.2.0/24' + }); + }); + + test('creates NAT Gateway for private subnet connectivity', () => { + template.hasResourceProperties('AWS::EC2::NatGateway', {}); + }); + + test('creates Internet Gateway', () => { + template.hasResourceProperties('AWS::EC2::InternetGateway', {}); + }); + }); + + describe('Security Group', () => { + test('creates security group for Batch', () => { + template.hasResourceProperties('AWS::EC2::SecurityGroup', { + GroupDescription: 'Security group for OpenMP Batch compute environment', + SecurityGroupEgress: [{ + CidrIp: '0.0.0.0/0', + IpProtocol: '-1' + }] + }); + }); + }); + + describe('IAM Roles', () => { + test('creates Batch service role', () => { + template.hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [{ + Effect: 'Allow', + Principal: { Service: 'batch.amazonaws.com' }, + Action: 'sts:AssumeRole' + }] + }, + ManagedPolicyArns: [{ + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/service-role/AWSBatchServiceRole' + ]] + }] + }); + }); + + test('creates EC2 instance role', () => { + template.hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [{ + Effect: 'Allow', + Principal: { Service: 'ec2.amazonaws.com' }, + Action: 'sts:AssumeRole' + }] + }, + ManagedPolicyArns: [{ + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role' + ]] + }] + }); + }); + + test('creates job execution role', () => { + template.hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [{ + Effect: 'Allow', + Principal: { Service: 'ecs-tasks.amazonaws.com' }, + Action: 'sts:AssumeRole' + }] + }, + ManagedPolicyArns: [{ + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy' + ]] + }] + }); + }); + + + }); + + describe('AWS Batch Configuration', () => { + test('creates Batch compute environment', () => { + template.hasResourceProperties('AWS::Batch::ComputeEnvironment', { + Type: 'MANAGED', + State: 'ENABLED', + ComputeResources: { + Type: 'EC2', + MinvCpus: 0, + MaxvCpus: 256, + InstanceTypes: ['c6i.large', 'c6i.xlarge', 'c6i.2xlarge', 'c5.large', 'c5.xlarge'] + } + }); + }); + + test('creates Batch job queue', () => { + template.hasResourceProperties('AWS::Batch::JobQueue', { + State: 'ENABLED', + Priority: 1 + }); + }); + + test('creates Batch job definition', () => { + template.hasResourceProperties('AWS::Batch::JobDefinition', { + Type: 'container', + ContainerProperties: { + Vcpus: 2, + Memory: 4096 + } + }); + }); + }); + + describe('Lambda Function', () => { + test('creates Lambda function for job submission', () => { + // Filter out the custom resource Lambda functions + const lambdaFunctions = template.findResources('AWS::Lambda::Function'); + const jobSubmitterFunctions = Object.entries(lambdaFunctions).filter(([key, value]: [string, any]) => { + return key.includes('JobSubmitterFunction') || + (value.Properties?.Runtime === 'python3.11'); + }); + + expect(jobSubmitterFunctions.length).toBeGreaterThanOrEqual(1); + + // Check for the main job submitter function properties + template.hasResourceProperties('AWS::Lambda::Function', { + Runtime: 'python3.11', + Handler: 'index.handler', + Timeout: 300 + }); + }); + + test('grants Lambda permissions for Batch operations', () => { + // Check that the Lambda function has Batch permissions + const policies = template.findResources('AWS::IAM::Policy'); + const jobSubmitterPolicy = Object.values(policies).find((policy: any) => + policy.Properties?.PolicyName?.includes('JobSubmitterFunction') + ); + + expect(jobSubmitterPolicy).toBeDefined(); + const statements = (jobSubmitterPolicy as any).Properties.PolicyDocument.Statement; + + // Find the statement with Batch permissions + const batchStatement = statements.find((stmt: any) => + Array.isArray(stmt.Action) && stmt.Action.some((action: string) => action.startsWith('batch:')) + ); + + expect(batchStatement).toBeDefined(); + expect(batchStatement.Action).toEqual(expect.arrayContaining([ + 'batch:SubmitJob', + 'batch:DescribeJobs', + 'batch:ListJobs' + ])); + }); + }); + + describe('CloudWatch Logs', () => { + test('creates CloudWatch log group', () => { + template.hasResourceProperties('AWS::Logs::LogGroup', { + RetentionInDays: 7 + }); + }); + }); + + describe('Stack Outputs', () => { + test('exports important resource identifiers', () => { + const outputs = template.findOutputs('*'); + + expect(outputs).toHaveProperty('ECRRepositoryURI'); + expect(outputs).toHaveProperty('JobQueueName'); + expect(outputs).toHaveProperty('JobDefinitionName'); + expect(outputs).toHaveProperty('LambdaFunctionName'); + }); + }); + + describe('Resource Count Validation', () => { + test('creates expected number of resources', () => { + // Verify we have the main resource types + expect(Object.keys(template.findResources('AWS::ECR::Repository'))).toHaveLength(1); + expect(Object.keys(template.findResources('AWS::EC2::VPC'))).toHaveLength(1); + expect(Object.keys(template.findResources('AWS::Batch::ComputeEnvironment'))).toHaveLength(1); + expect(Object.keys(template.findResources('AWS::Batch::JobQueue'))).toHaveLength(1); + expect(Object.keys(template.findResources('AWS::Batch::JobDefinition'))).toHaveLength(1); + expect(Object.keys(template.findResources('AWS::Logs::LogGroup')).length).toBeGreaterThanOrEqual(1); + + // Check Lambda functions - should have our job submitter plus potentially custom resource handlers + const lambdaFunctions = Object.keys(template.findResources('AWS::Lambda::Function')); + expect(lambdaFunctions.length).toBeGreaterThanOrEqual(1); + + // Verify we have multiple IAM roles as expected + expect(Object.keys(template.findResources('AWS::IAM::Role')).length).toBeGreaterThanOrEqual(3); + }); + }); + + describe('Script Files', () => { + test('submit-job script exists and is executable', () => { + const fs = require('fs'); + const path = require('path'); + + const scriptPath = path.join(__dirname, '..', 'scripts', 'submit-job.sh'); + expect(fs.existsSync(scriptPath)).toBe(true); + + // Check if file is executable (has execute permission) + const stats = fs.statSync(scriptPath); + expect(stats.mode & parseInt('111', 8)).toBeGreaterThan(0); + }); + + test('build-and-deploy script includes deployment info capture', () => { + const fs = require('fs'); + const path = require('path'); + + const scriptPath = path.join(__dirname, '..', 'scripts', 'build-and-deploy.sh'); + const scriptContent = fs.readFileSync(scriptPath, 'utf8'); + + // Check that the script contains deployment info capture logic + expect(scriptContent).toContain('deployment-info.json'); + expect(scriptContent).toContain('stackOutputs'); + expect(scriptContent).toContain('awsProfile'); + }); + + test('package.json follows standard format', () => { + const fs = require('fs'); + const path = require('path'); + + const packagePath = path.join(__dirname, '..', 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')); + + // Check standard fields + expect(packageJson).toHaveProperty('name', 'batch-ecr-lambda'); + expect(packageJson).toHaveProperty('description'); + expect(packageJson).toHaveProperty('private', true); + expect(packageJson).toHaveProperty('author'); + expect(packageJson).toHaveProperty('license', 'Apache-2.0'); + + // Check standard scripts + expect(packageJson.scripts).toHaveProperty('build', 'tsc'); + expect(packageJson.scripts).toHaveProperty('watch', 'tsc -w'); + expect(packageJson.scripts).toHaveProperty('test', 'jest'); + expect(packageJson.scripts).toHaveProperty('cdk', 'cdk'); + }); + + test('gitignore includes deployment-info.json', () => { + const fs = require('fs'); + const path = require('path'); + + const gitignorePath = path.join(__dirname, '..', '.gitignore'); + const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8'); + + expect(gitignoreContent).toContain('deployment-info.json'); + }); + }); + + describe('Security Best Practices', () => { + + + test('IAM roles follow least privilege principle', () => { + // Verify no roles have admin access + const policies = template.findResources('AWS::IAM::Policy'); + Object.values(policies).forEach((policy: any) => { + const statements = policy.Properties?.PolicyDocument?.Statement || []; + statements.forEach((statement: any) => { + // Check that we don't have overly broad permissions + if (Array.isArray(statement.Action)) { + expect(statement.Action).not.toContain('*:*'); + } else if (typeof statement.Action === 'string') { + expect(statement.Action).not.toBe('*:*'); + } + }); + }); + }); + + test('VPC uses private subnets for compute resources', () => { + // Verify Batch compute environment uses subnets (should be private ones) + const computeEnv = template.findResources('AWS::Batch::ComputeEnvironment'); + const computeEnvProps = Object.values(computeEnv)[0] as any; + + expect(computeEnvProps.Properties.ComputeResources.Subnets).toBeDefined(); + expect(Array.isArray(computeEnvProps.Properties.ComputeResources.Subnets)).toBe(true); + expect(computeEnvProps.Properties.ComputeResources.Subnets.length).toBeGreaterThan(0); + + // Verify these are references to private subnets (they should contain 'PrivateSubnet' in the reference) + const subnetRefs = computeEnvProps.Properties.ComputeResources.Subnets; + subnetRefs.forEach((subnetRef: any) => { + expect(subnetRef.Ref).toMatch(/.*PrivateSubnet.*/); + }); + }); + }); +}); diff --git a/typescript/batch-ecr-openmp/tsconfig.json b/typescript/batch-ecr-openmp/tsconfig.json new file mode 100644 index 000000000..28bb557fa --- /dev/null +++ b/typescript/batch-ecr-openmp/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": [ + "es2022" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +} From d551100f8fd9315d6ed672801417307441be114b Mon Sep 17 00:00:00 2001 From: architec <32494274+architec@users.noreply.github.com> Date: Sat, 21 Jun 2025 01:32:17 -0700 Subject: [PATCH 2/5] feat(typescript): add AWS Batch with ECR and Lambda example for OpenMP benchmarks --- .../src/openmp/openmp_benchmark | Bin 60720 -> 0 bytes .../batch-ecr-openmp/src/openmp/test_benchmark | Bin 58672 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 typescript/batch-ecr-openmp/src/openmp/openmp_benchmark delete mode 100644 typescript/batch-ecr-openmp/src/openmp/test_benchmark diff --git a/typescript/batch-ecr-openmp/src/openmp/openmp_benchmark b/typescript/batch-ecr-openmp/src/openmp/openmp_benchmark deleted file mode 100644 index 60ed5893e6b52fefe06325376375077139f26b29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60720 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 From 6e400f27a383559e949776f06015e7b831802bfb Mon Sep 17 00:00:00 2001 From: "David L." <32494274+architec@users.noreply.github.com> Date: Sat, 21 Jun 2025 01:41:21 -0700 Subject: [PATCH 3/5] Update package.json --- typescript/batch-ecr-openmp/package.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/typescript/batch-ecr-openmp/package.json b/typescript/batch-ecr-openmp/package.json index 9f388d9ee..1088ef7f7 100644 --- a/typescript/batch-ecr-openmp/package.json +++ b/typescript/batch-ecr-openmp/package.json @@ -12,10 +12,6 @@ "test": "jest", "cdk": "cdk" }, - "author": { - "name": "Amazon Web Services", - "url": "https://aws.amazon.com" - }, "license": "Apache-2.0", "devDependencies": { "@types/jest": "^29.5.14", From 6891c4e5f3ee2d00ff78c0481f61339922c21992 Mon Sep 17 00:00:00 2001 From: "David L." <32494274+architec@users.noreply.github.com> Date: Sat, 21 Jun 2025 01:42:32 -0700 Subject: [PATCH 4/5] Update package.json --- typescript/batch-ecr-openmp/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/batch-ecr-openmp/package.json b/typescript/batch-ecr-openmp/package.json index 1088ef7f7..aeb9e96af 100644 --- a/typescript/batch-ecr-openmp/package.json +++ b/typescript/batch-ecr-openmp/package.json @@ -1,5 +1,5 @@ { - "name": "batch-ecr-lambda", + "name": "batch-ecr-openmp", "version": "1.0.0", "description": "AWS Batch with containerized application in ECR and Lambda for job submission", "private": true, From c5a78de540fc50e57f6ffcaa85ec2cee0a31f080 Mon Sep 17 00:00:00 2001 From: "David L." <32494274+architec@users.noreply.github.com> Date: Sat, 21 Jun 2025 01:59:13 -0700 Subject: [PATCH 5/5] Update aws-batch-openmp-benchmark.test.ts --- .../test/aws-batch-openmp-benchmark.test.ts | 43 ------------------- 1 file changed, 43 deletions(-) diff --git a/typescript/batch-ecr-openmp/test/aws-batch-openmp-benchmark.test.ts b/typescript/batch-ecr-openmp/test/aws-batch-openmp-benchmark.test.ts index daff06f15..e7e67f72f 100644 --- a/typescript/batch-ecr-openmp/test/aws-batch-openmp-benchmark.test.ts +++ b/typescript/batch-ecr-openmp/test/aws-batch-openmp-benchmark.test.ts @@ -260,18 +260,6 @@ describe('AWS Batch OpenMP Benchmark Stack', () => { }); describe('Script Files', () => { - test('submit-job script exists and is executable', () => { - const fs = require('fs'); - const path = require('path'); - - const scriptPath = path.join(__dirname, '..', 'scripts', 'submit-job.sh'); - expect(fs.existsSync(scriptPath)).toBe(true); - - // Check if file is executable (has execute permission) - const stats = fs.statSync(scriptPath); - expect(stats.mode & parseInt('111', 8)).toBeGreaterThan(0); - }); - test('build-and-deploy script includes deployment info capture', () => { const fs = require('fs'); const path = require('path'); @@ -284,37 +272,6 @@ describe('AWS Batch OpenMP Benchmark Stack', () => { expect(scriptContent).toContain('stackOutputs'); expect(scriptContent).toContain('awsProfile'); }); - - test('package.json follows standard format', () => { - const fs = require('fs'); - const path = require('path'); - - const packagePath = path.join(__dirname, '..', 'package.json'); - const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')); - - // Check standard fields - expect(packageJson).toHaveProperty('name', 'batch-ecr-lambda'); - expect(packageJson).toHaveProperty('description'); - expect(packageJson).toHaveProperty('private', true); - expect(packageJson).toHaveProperty('author'); - expect(packageJson).toHaveProperty('license', 'Apache-2.0'); - - // Check standard scripts - expect(packageJson.scripts).toHaveProperty('build', 'tsc'); - expect(packageJson.scripts).toHaveProperty('watch', 'tsc -w'); - expect(packageJson.scripts).toHaveProperty('test', 'jest'); - expect(packageJson.scripts).toHaveProperty('cdk', 'cdk'); - }); - - test('gitignore includes deployment-info.json', () => { - const fs = require('fs'); - const path = require('path'); - - const gitignorePath = path.join(__dirname, '..', '.gitignore'); - const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8'); - - expect(gitignoreContent).toContain('deployment-info.json'); - }); }); describe('Security Best Practices', () => {