├── src
├── vite-env.d.ts
├── server
│ ├── types.ts
│ ├── tsconfig-server.json
│ ├── server.ts
│ ├── routes.ts
│ ├── utils
│ │ └── utils.ts
│ └── controllers
│ │ ├── dbController.ts
│ │ └── lambdaController.ts
├── style.css
├── main.tsx
├── vite.config.ts
├── components
│ ├── loadingBar.tsx
│ ├── style.css
│ ├── results.tsx
│ ├── graph.tsx
│ ├── graphDetailed.tsx
│ └── chakraForm.tsx
├── store.ts
├── formData
│ ├── infoAPI.ts
│ ├── sseSlice.ts
│ ├── infoSlice.ts
│ └── resultsSlice.ts
├── scripts
│ ├── packer
│ │ └── aws-ubuntu.pkr.hcl
│ └── cloudFormationDeploy.yaml
└── App.tsx
├── vite.config.ts
├── tsconfig.node.json
├── .gitignore
├── index.html
├── .eslintrc.cjs
├── tsconfig.json
├── package.json
├── README.md
└── DEPLOYMENT.md
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/server/types.ts:
--------------------------------------------------------------------------------
1 | export default interface CustomError extends Error {
2 | status?: number;
3 | requestDetails?: { body: string };
4 |
5 | }
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": ["vite.config.ts"]
9 | }
--------------------------------------------------------------------------------
/src/style.css:
--------------------------------------------------------------------------------
1 | .body {
2 | background-color: rgb(237, 38, 217);
3 | }
4 |
5 | .flex-container {
6 | display: flex;
7 | position: absolute;
8 | /* top: 25%; */
9 | height: 100vh;
10 | background-color: rgba(40, 57, 206, 0.23);
11 | }
--------------------------------------------------------------------------------
/src/server/tsconfig-server.json:
--------------------------------------------------------------------------------
1 | // tsconfig-server.json
2 | {
3 | "compilerOptions": {
4 | "target": "ESNext",
5 | "module": "NodeNext",
6 | "outDir": "../../dist/serverDist",
7 | "esModuleInterop": true,
8 | "moduleResolution": "NodeNext"
9 | // Add any other necessary compiler options
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App'
4 | import store from './store'
5 | import { Provider } from 'react-redux'
6 |
7 |
8 | ReactDOM.createRoot(document.getElementById('root')!).render(
9 |
10 |
11 |
12 |
13 |
14 | )
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 | src/server/serverDist
15 |
16 | .env
17 | # Editor directories and files
18 | .vscode/*
19 | !.vscode/extensions.json
20 | .idea
21 | .DS_Store
22 | *.suo
23 | *.ntvs*
24 | *.njsproj
25 | *.sln
26 | *.sw?
27 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Shear
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/vite.config.ts:
--------------------------------------------------------------------------------
1 | // vite.config.js
2 | import { defineConfig } from 'vite';
3 | import react from '@vitejs/plugin-react';
4 |
5 | export default defineConfig({
6 | plugins: [react()],
7 | server: {
8 | proxy: {
9 | '/api': {
10 | target: 'http://localhost:3000',
11 | changeOrigin: true,
12 | rewrite: (path) => path.replace(/^\/api/, ''),
13 | },
14 | },
15 | },
16 | build: {
17 | outDir: 'build',
18 | },
19 | });
20 |
--------------------------------------------------------------------------------
/src/components/loadingBar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import * as ChakraUI from '@chakra-ui/react'
4 |
5 | const LoadingBar: React.FC = () => {
6 | const dispatch = useDispatch();
7 | return (
8 |
9 |
10 |
11 |
12 |
)
13 | };
14 | export default LoadingBar;
15 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:@typescript-eslint/recommended',
7 | 'plugin:react-hooks/recommended',
8 | ],
9 | ignorePatterns: ['dist', '.eslintrc.cjs'],
10 | parser: '@typescript-eslint/parser',
11 | plugins: ['react-refresh'],
12 | rules: {
13 | 'react-refresh/only-export-components': [
14 | 'warn',
15 | { allowConstantExport: true },
16 | ],
17 | },
18 | }
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "module": "ES2022",
6 | "moduleResolution": "node",
7 | "noEmit": true,
8 | "jsx": "react-jsx",
9 | "allowImportingTsExtensions": true,
10 | "esModuleInterop": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "useUnknownInCatchVariables": false,
13 | "skipLibCheck": true,
14 | "outDir": "./dist/",
15 | "rootDir": "./src"
16 | },
17 | "include": ["src"],
18 | "exclude": ["vite.config.ts"]
19 | }
20 |
--------------------------------------------------------------------------------
/src/store.ts:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import infoReducer, {FormValues} from "./formData/infoSlice";
3 | import resultsReducer, {ResultValues} from './formData/resultsSlice'
4 | import sseReducer, {SseState} from "./formData/sseSlice";
5 |
6 | export interface RootState {
7 | info: FormValues
8 | results: ResultValues
9 | sse: SseState
10 | }
11 |
12 | export const store = configureStore({
13 | reducer: {
14 | info: infoReducer,
15 | results: resultsReducer,
16 | sse: sseReducer
17 | }
18 | })
19 |
20 | export default store;
--------------------------------------------------------------------------------
/src/formData/infoAPI.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosResponse } from "axios";
2 | import { FormValues } from "./infoSlice";
3 |
4 | axios.defaults.baseURL = "http://localhost:3000/api"
5 |
6 | export const optimizerAPI = {
7 |
8 | runOptimizerFunc: (form: FormValues): Promise => {
9 | const stringifiedFormData = JSON.stringify(form)
10 | console.log(stringifiedFormData)
11 | return axios.post('executeLambdaWorkflow', stringifiedFormData, {
12 | headers: {
13 | 'Content-Type': 'application/json',
14 | },
15 | });
16 | },
17 | }
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/formData/sseSlice.ts:
--------------------------------------------------------------------------------
1 | import { PayloadAction, createSlice, current, createAsyncThunk, Dispatch } from "@reduxjs/toolkit";
2 |
3 |
4 |
5 |
6 |
7 | export interface SseState {
8 | data: string[]
9 | }
10 |
11 | const sseState: SseState = {
12 | data: [],
13 | }
14 |
15 |
16 |
17 | const sseSlice = createSlice({
18 | name: 'sse',
19 | initialState: sseState,
20 | reducers: {
21 | addData(state, action: PayloadAction) {
22 | state.data.push(action.payload)
23 | console.log(current(state.data))
24 | },
25 | },
26 |
27 | })
28 |
29 |
30 |
31 |
32 | export const {addData} = sseSlice.actions;
33 |
34 | export default sseSlice.reducer;
--------------------------------------------------------------------------------
/src/components/style.css:
--------------------------------------------------------------------------------
1 | #root {
2 | /* max-width: 1280px; */
3 | margin: 0 auto;
4 | /* padding: 2rem; */
5 | text-align: center;
6 | height: 100%;
7 | /* background-color: rgba(38, 228, 35, 0.23); */
8 | }
9 |
10 | /* .body {
11 | display: flex;
12 | background-color: rgba(211, 211, 211, 0.23);
13 | } */
14 |
15 | .chakra-ui-light {
16 | /* background-color: rgba(86, 181, 91, 0.284); */
17 | }
18 |
19 | h2 {
20 | font-size: 24px;
21 | }
22 |
23 | .graphMain {
24 |
25 | padding: 100px;
26 | padding-bottom: 30%;
27 | margin: 0 auto;
28 | height: 80vh;
29 | width: 45vw;
30 | }
31 |
32 | .graphDetailed {
33 |
34 | padding: 100px;
35 | padding-bottom: 30%;
36 | margin: 0 auto;
37 | height: 80vh;
38 | width: 45vw;
39 | }
40 |
41 | .button:active {
42 | background-color: #3498db;
43 | color: #fff;
44 | transform: scale(0.95);
45 | }
--------------------------------------------------------------------------------
/src/scripts/packer/aws-ubuntu.pkr.hcl:
--------------------------------------------------------------------------------
1 | packer {
2 | required_plugins {
3 | amazon = {
4 | version = ">= 1.2.8"
5 | source = "github.com/hashicorp/amazon"
6 | }
7 | }
8 | }
9 |
10 | source "amazon-ebs" "ubuntu" {
11 | ami_name = "shear-v4"
12 | ami_groups = ["all"]
13 | instance_type = "t2.micro"
14 | region = "us-east-2"
15 | source_ami_filter {
16 | filters = {
17 | name = "ubuntu/images/*ubuntu-jammy-22.04-amd64-server-*"
18 | root-device-type = "ebs"
19 | virtualization-type = "hvm"
20 | }
21 | most_recent = true
22 | owners = ["099720109477"]
23 | }
24 | ssh_username = "ubuntu"
25 | }
26 |
27 | build {
28 | name = "packerv1"
29 | sources = [
30 | "source.amazon-ebs.ubuntu"
31 | ]
32 |
33 | provisioner "file" {
34 | source = "/Users/alby/Desktop/shear/dist"
35 | destination = "/home/ubuntu/"
36 | }
37 | }
--------------------------------------------------------------------------------
/src/components/results.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { RootState } from '../store';
4 | import axios from 'axios';
5 | axios.defaults.baseURL = "http://localhost:3000/api"
6 |
7 | const Results: React.FC = () => {
8 | let source;
9 | useEffect(() => {
10 | const startSSE = () => {
11 | source = new EventSource('http://localhost:3000/api/LambdaWorkflowSSE')
12 | if (source) {
13 | console.log('source connected', source)
14 | }
15 | source.onmessage = (message) => {
16 | console.log('this is msg',message.data)
17 | }
18 | }
19 | startSSE();
20 | }, [])
21 |
22 | return (
23 |
24 | {/*
SSE Data:
*/}
25 |
26 | {/* {sseData.map((data, index) => (
27 | - {JSON.stringify(data)}
28 | ))} */}
29 |
30 |
31 | );
32 | };
33 |
34 | export default Results;
35 |
--------------------------------------------------------------------------------
/src/server/server.ts:
--------------------------------------------------------------------------------
1 | import express, { Request, Response, NextFunction } from 'express'
2 | import {router} from "./routes.ts"
3 | import path from "path"
4 | import { fileURLToPath } from 'url';
5 | import CustomError from './types';
6 |
7 | const PORT = 3000;
8 | const app = express();
9 | const __filename = fileURLToPath(import.meta.url);
10 | const __dirname = path.dirname(__filename);
11 |
12 | app.use(express.json());
13 | app.use((req, res, next) => {
14 | res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5173');
15 | res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
16 | res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
17 | res.setHeader('Cache-Control', 'no-store');
18 |
19 | next();
20 | });
21 |
22 |
23 | app.use("/api", router);
24 | app.use(express.static(path.join(__dirname, '../../dist')));
25 | app.use((err: CustomError, req: Request, res: Response, _next: NextFunction) => {
26 | console.error(err);
27 |
28 | const status = err.status || 500;
29 | res.status(status).json({
30 | error: {
31 | message: err.message || 'An unexpected middleware error occurred. ',
32 | status,
33 | requestDetails: err.requestDetails || {},
34 | },
35 | });
36 | });
37 |
38 |
39 | app.listen(PORT, () => console.log('CONNECTED: listening on PORT', PORT));
--------------------------------------------------------------------------------
/src/server/routes.ts:
--------------------------------------------------------------------------------
1 | import express, { Request, Response } from 'express';
2 | export const router = express.Router();
3 | import lambdaController from './controllers/lambdaController.ts'
4 | import {getLambdaLogs, addLambdaLog} from "./controllers/dbController.ts"
5 | import { EventEmitter } from 'events';
6 |
7 | export const myEventEmitter = new EventEmitter();
8 |
9 | router.get('/LambdaWorkflowSSE', (req: Request, res: Response): void => {
10 |
11 |
12 | res.setHeader('Content-Type', 'text/event-stream');
13 | res.setHeader('Cache-Control', 'no-cache');
14 | res.setHeader('Connection', 'keep-alive');
15 |
16 | const sendUpdate = (data) => {
17 | res.write(`data: ${JSON.stringify({ message: data })}\n\n`)
18 | }
19 |
20 | myEventEmitter.on('update', sendUpdate )
21 |
22 | // console.log(myEventEmitter.eventNames())
23 | req.on('close', () => {
24 | myEventEmitter.off('update', sendUpdate);
25 | console.log('Client connection closed');
26 | });
27 | });
28 |
29 | router.post('/getLambdaLogs', lambdaController.shear, (req: Request, res: Response): void => {
30 | res.status(200).json(res.locals.output)
31 | })
32 |
33 | // Executing "step function workflow"
34 | router.post("/executeLambdaWorkflow", lambdaController.shear, addLambdaLog, (req: Request, res: Response): void => {
35 | console.log(req.body)
36 | res.status(200).json(res.locals.output)
37 | })
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import ChakraForm from "./components/chakraForm"
3 | import Graph from "./components/graph"
4 | import GraphDetailed from "./components/graphDetailed"
5 | import { Box, Text, Flex, Spacer, Stack, HStack, Grid, Center, ChakraProvider, extendBaseTheme, theme as chakraTheme, } from '@chakra-ui/react'
6 | // import chakraTheme from '@chakra-ui/theme'
7 | import './style.css'
8 | import Results from "./components/results"
9 |
10 | const { Button } = chakraTheme.components
11 |
12 | const theme = extendBaseTheme({
13 | components: {
14 | Button,
15 | },
16 | })
17 |
18 | const App: React.FC = () => {
19 |
20 |
21 | return (
22 |
23 |
24 |
25 |
26 | Shear
27 | an AWS Lambda function optimizer
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | Standard Test Results
36 |
37 |
38 |
39 | Fine-Tuned Test Results
40 |
41 |
42 |
43 |
44 |
45 | {/* */}
46 |
47 |
48 |
49 |
50 | )
51 | }
52 |
53 | export default App
54 |
--------------------------------------------------------------------------------
/src/server/utils/utils.ts:
--------------------------------------------------------------------------------
1 | import CustomError from "../types.js"
2 |
3 | export function wait(ms) {
4 | return new Promise((resolve) => setTimeout(resolve, ms));
5 | }
6 |
7 | export function calculateCosts(resultObj: { [key: number]: number }): { [key: number]: number } {
8 | const newObj: { [key: number]: number } = {};
9 |
10 | for (const key in resultObj) {
11 | if (Object.prototype.hasOwnProperty.call(resultObj, key)) {
12 | const originalValue = resultObj[key];
13 |
14 | const megabytesToGigabytes = Number(key) / 1024; // 1 GB = 1024 MB
15 |
16 |
17 | const millisecondsToSeconds = originalValue / 1000;
18 |
19 | // Calculate the new value in gigabyte-seconds
20 | //PER THOUSAND INVOCATIONS
21 | const newValue = megabytesToGigabytes * millisecondsToSeconds * 0.0000166667 * 1000;
22 | newObj[Number(key)] = newValue;
23 | }
24 | }
25 |
26 | return newObj;
27 | }
28 |
29 | export function extractBilledDurationFrom64(logText) {
30 |
31 | const billedDurationRegex = /Billed Duration: (\d+(\.\d+)?) ms/;
32 | const match = logText.match(billedDurationRegex);
33 |
34 |
35 | if (match) {
36 |
37 | return match[1]
38 | } else {
39 | return 'error!'
40 | }
41 | }
42 |
43 | export function reduceObjectToMedian(inputObj: object) {
44 | const result = {};
45 |
46 | for (const key in inputObj) {
47 | if (Object.prototype.hasOwnProperty.call(inputObj, key)) {
48 | const values = inputObj[key].map(Number);
49 | values.sort((a, b) => a - b); //sort
50 |
51 | let median;
52 | const middle = Math.floor(values.length / 2);
53 |
54 | if (values.length % 2 === 0) {
55 | median = (values[middle - 1] + values[middle]) / 2;
56 | } else {
57 | median = values[middle];
58 | }
59 |
60 | result[key] = median;
61 | }
62 | }
63 |
64 | return result;
65 | }
66 |
67 | export function createCustomError(message, status, requestDetails) {
68 | const error:CustomError = new Error(message);
69 | error.status = status;
70 | error.requestDetails = requestDetails;
71 | return error;
72 | }
73 |
74 | export function getRegionFromARN(arn) {
75 | const arnParts = arn.split(':');
76 | if (arnParts.length >= 4) {
77 | return arnParts[3];
78 | } else {
79 | return null;
80 | }
81 | }
--------------------------------------------------------------------------------
/src/formData/infoSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction, createAsyncThunk, current } from "@reduxjs/toolkit";
2 | import { optimizerAPI } from "./infoAPI";
3 |
4 | export interface FormValues {
5 | name: string;
6 | ARN: string;
7 | memoryArray: (string | number)[];
8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
9 | functionPayload: Record;
10 | volume: (number);
11 | recursiveSearch: (boolean);
12 | concurrent: (boolean);
13 | }
14 |
15 | export const loadData = createAsyncThunk('data/data', async (formData: FormValues) => {
16 | const response = await optimizerAPI.runOptimizerFunc(formData);
17 | return response.data;
18 | })
19 |
20 | const formValues: FormValues = {
21 | name: 'Bob',
22 | ARN: '',
23 | memoryArray: [],
24 | functionPayload: {},
25 | volume: 10,
26 | recursiveSearch: true,
27 | concurrent: true,
28 | };
29 |
30 | const infoSlice = createSlice({
31 | name: 'info',
32 | initialState: formValues,
33 | reducers: {
34 | nameInput(state, action: PayloadAction) {
35 | state.name = action.payload;
36 | },
37 | arnInput(state, action: PayloadAction) {
38 | state.ARN = action.payload;
39 | },
40 | funcParamsInput(state, action: PayloadAction) {
41 | const parsedPayLoad = JSON.parse(action.payload)
42 | state.functionPayload = parsedPayLoad;
43 | },
44 |
45 | powerValueInput(state, action: PayloadAction) {
46 | state.memoryArray = getMedians(action.payload)
47 | },
48 |
49 | testVolInput(state, action: PayloadAction) {
50 | state.volume = action.payload;
51 | },
52 |
53 | checksInput(state, action: PayloadAction) {
54 | state.recursiveSearch = action.payload[0];
55 | state.concurrent = action.payload[1];
56 | },
57 | },
58 | });
59 |
60 | function getMedians(array: string[]): number[] {
61 | const arrayOfVals: number[] = array.map(Number)
62 | const min = arrayOfVals[0]
63 | const max = arrayOfVals[1]
64 | const incrementVals = []
65 | const increments = (max - min) / (arrayOfVals[2] + 1)
66 | for (let i = 1; i <= arrayOfVals[2]; i++) {
67 | incrementVals.push(Math.floor(min + (i * increments)))
68 | }
69 | const median = Math.ceil((min + max) / 2)
70 | const lowerMedian = Math.ceil((min + median) / 2)
71 | const upperMedian = Math.ceil((median + max) / 2)
72 | const result: number[] = [arrayOfVals[0], arrayOfVals[1], ...incrementVals]
73 | return result.sort((a, b) => a - b)
74 | }
75 |
76 | export const { nameInput, arnInput, funcParamsInput, powerValueInput, testVolInput, checksInput } = infoSlice.actions;
77 | export default infoSlice.reducer;
78 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "shear",
3 | "private": true,
4 | "version": "1.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "serve": "node --loader ts-node/esm src/server/server.ts",
9 | "dev-external": "vite --host 0.0.0.0 --port 5173",
10 | "serve-external": "node --no-warnings --loader ts-node/esm src/server/server.ts --host 0.0.0.0 --port 3000",
11 | "all": "concurrently \"npm run serve\" \"npm run dev\"",
12 | "build": "tsc && vite build",
13 | "build-sketchy": "vite build",
14 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
15 | "preview": "vite preview",
16 | "build-server": "tsc -p src/server/tsconfig-server.json",
17 | "run-server-ec2": "node serverDist/server.js",
18 | "run-server-local": "node dist/serverDist/server.js",
19 | "deploy": "aws cloudformation create-stack --region us-east-2 --stack-name testprogram9242 --template-body file://./src/scripts/cloudFormationDeploy.yaml --capabilities CAPABILITY_IAM",
20 | "packer": "packer build src/scripts/packer/aws-ubuntu.pkr.hcl"
21 | },
22 | "dependencies": {
23 | "@aws-sdk/client-cloudwatch-logs": "^3.465.0",
24 | "@aws-sdk/client-dynamodb": "^3.495.0",
25 | "@aws-sdk/client-lambda": "^3.465.0",
26 | "@aws-sdk/util-dynamodb": "^3.495.0",
27 | "@aws-sdk/util-utf8-node": "^3.259.0",
28 | "@chakra-ui/react": "^2.8.2",
29 | "@emotion/react": "^11.11.1",
30 | "@emotion/styled": "^11.11.0",
31 | "@fontsource/roboto": "^5.0.8",
32 | "@mui/material": "^5.14.19",
33 | "@reduxjs/toolkit": "^1.9.7",
34 | "adm-zip": "^0.5.10",
35 | "aws-sdk": "^2.1530.0",
36 | "axios": "^1.6.2",
37 | "concurrently": "^8.2.2",
38 | "cors": "^2.8.5",
39 | "dotenv": "^16.3.1",
40 | "express": "^4.18.2",
41 | "final-form": "^4.20.10",
42 | "fs": "^0.0.1-security",
43 | "https": "^1.0.0",
44 | "marshall": "^1.3.2",
45 | "nodemon": "^3.0.3",
46 | "prettier": "^3.1.0",
47 | "react": "^18.2.0",
48 | "react-d3-library": "^1.1.8",
49 | "react-dom": "^18.2.0",
50 | "react-final-form": "^6.5.9",
51 | "react-redux": "^8.1.3",
52 | "react-router-dom": "^6.19.0",
53 | "recharts": "^2.10.3",
54 | "uuid": "^9.0.1"
55 | },
56 | "devDependencies": {
57 | "@types/d3": "^7.4.3",
58 | "@types/express": "^4.17.21",
59 | "@types/react": "^18.2.37",
60 | "@types/react-dom": "^18.2.15",
61 | "@typescript-eslint/eslint-plugin": "^6.10.0",
62 | "@typescript-eslint/parser": "^6.10.0",
63 | "@vitejs/plugin-react": "^4.2.1",
64 | "concurrently": "^8.2.1",
65 | "eslint": "^8.53.0",
66 | "eslint-plugin-react-hooks": "^4.6.0",
67 | "eslint-plugin-react-refresh": "^0.4.4",
68 | "ts-node": "^10.9.1",
69 | "typescript": "5.0",
70 | "vite": "^5.0.0"
71 | }
72 | }
--------------------------------------------------------------------------------
/src/formData/resultsSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
2 | import { optimizerAPI } from "./infoAPI";
3 | import { FormValues } from "./infoSlice";
4 |
5 |
6 | export interface ResultValues {
7 | billedDurationOutput: Record;
8 | costOutput: Record;
9 | CostData: (string | number)[];
10 | TimeData: (string | number)[];
11 | MemoryData: (string | number)[];
12 | DetailedCostData: (string | number)[];
13 | DetailedTimeData: (string | number)[];
14 | DetailedMemoryData: (string | number)[];
15 | }
16 |
17 | //function sends form data to backend to get spun up. Back end will need to send back the data from spinning the algo. -JK
18 | export const runOptimizer = createAsyncThunk('results/data', async (Formdata) => {
19 | const response = await optimizerAPI.runOptimizerFunc(Formdata);
20 | // console.log(response)
21 | // console.log(response.data)
22 | return response.data;
23 | });
24 |
25 |
26 |
27 | export const getAllData = createAsyncThunk
28 |
29 | //current state of return data, Double check what we are expecting(discuss with backend)- JK
30 | const initialState: ResultValues = {
31 | billedDurationOutput: {},
32 | costOutput: {},
33 | CostData: [190, 180, 215, 170, 160, 195, 220, 230, 185, 190, 210],
34 | TimeData: [500, 420, 350, 290, 240, 200, 170, 150, 135, 125, 120],
35 | MemoryData: [128, 256, 384, 512, 640, 768, 896, 1024, 1152, 1280, 2048],
36 | DetailedCostData: [170, 168, 163, 164, 161, 156, 162, 160, 160],
37 | DetailedTimeData: [290, 286, 278, 274, 266, 248, 250, 244, 240],
38 | DetailedMemoryData: [512, 528, 544, 560, 576, 592, 608, 624, 640],
39 | };
40 |
41 | const resultsSlice = createSlice({
42 | name: 'results',
43 | initialState,
44 | reducers: {},
45 | extraReducers: (builder) => {
46 | builder
47 | .addCase(runOptimizer.fulfilled, (state, action) => {
48 | // console.log("Result incoming")
49 |
50 | const rawResults = action.payload;
51 | const rawMemory = Object.keys(rawResults.billedDurationOutput).map((x) => parseInt(x));
52 | const detailedMemory = Object.keys(rawResults.bonusData.billedDurationOutput).map((x) => parseInt(x));
53 | state.MemoryData.splice(0, state.MemoryData.length, ...rawMemory);
54 | state.CostData.splice(0, state.CostData.length, ...Object.values(rawResults.costOutput));
55 | state.TimeData.splice(0, state.TimeData.length, ...Object.values(rawResults.billedDurationOutput));
56 | state.DetailedMemoryData.splice(0, state.MemoryData.length, ...detailedMemory);
57 | state.DetailedCostData.splice(0, state.CostData.length, ...Object.values(rawResults.bonusData.costOutput));
58 | state.DetailedTimeData.splice(0, state.TimeData.length, ...Object.values(rawResults.bonusData.billedDurationOutput));
59 | console.log(state)
60 | // return action.payload;
61 | })
62 | .addCase(runOptimizer.pending, (state, action) => {
63 | // console.log(action.payload)
64 | })
65 | .addCase(runOptimizer.rejected, (state) => {
66 |
67 | });
68 | },
69 | });
70 |
71 | export default resultsSlice.reducer;
72 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Shear
2 |
3 | ## Table of Contents
4 |
5 | - [About](#about)
6 | - [Inputs](#inputs)
7 | - [Outputs](#outputs)
8 | - [Local Development](#local-development)
9 | - [Deployment](#deployment)
10 | - [Contributing](#contributing)
11 | - [License](#license)
12 | - [Authors](#authors)
13 |
14 | ## About
15 |
16 | Shear is an AWS Lambda cost optimization tool designed to help you find the ideal balance between function runtime, memory usage, and cost efficiency. You can provide a Lambda function ARN as input, and Shear will invoke that function with multiple power configurations. It then analyzes the execution logs to suggest the best power configuration to minimize cost and/or maximize performance. It also supports concurrent invocations of the function for enhanced analysis.
17 |
18 | Please note that the input function will be executed in your AWS account.
19 |
20 | ## Inputs
21 |
22 | Typical input format:
23 |
24 | - Lambda ARN: `arn:aws:lambda:us-west-1:066290895578:function:testFunction`
25 | - Lambda Payload: `{"testFunctionParam1":3, "testFunctionParam2":2000, "testFunctionParam3":40}`
26 | - Memory Allocation:
27 | - Minimum (MB): 128
28 | - Maximum (MB): 4096
29 | - Memory Intervals: 5
30 | - Test Volume: 25
31 | - Concurrency: True
32 |
33 | ## Outputs
34 |
35 | The expected output is a graph showing the relationship between memory, time, and cost.
36 | If concurrency is enabled, an additional graph will be provided, displaying the same data but with fine-tuned power values.
37 |
38 | ## Local Development
39 |
40 | To get started with local development, install dev dependencies with `npm install`. To run the server and client concurrently, use `npm run all`.
41 |
42 | ## Deployment
43 |
44 | Three deployment options are available:
45 |
46 | - Option 1: AWS CloudFormation/HashiCorp Packer
47 | - Option 2: AWS Fargate via ECR
48 | - Option 3: Local
49 |
50 | For a detailed deployment breakdown, click [here](DEPLOYMENT.md).
51 |
52 | ## Contributing
53 |
54 | Shear welcomes contributions from the open source community. Submit Issues on GitHub to report bugs or suggest enhancements. To contribute code, fork this repo, commit your changes, and submit a Pull Request following our template. Follow Shear on [LinkedIn](https://www.linkedin.com/company/shearlambda) for updates.
55 |
56 | ### Encouraged Features
57 |
58 | - Saving analyzed functions to DB
59 | - Show ΔPerformance/ΔCost - interpolate data to curve, take derivative
60 | - Automatic optimizations via cron job (EventBridge)
61 |
62 | ### Known Bugs
63 |
64 | ## Authors
65 |
66 | - Albert Hu | [GitHub](https://github.com/albhu24) | [LinkedIn](https://www.linkedin.com/in/hu-albert/)
67 | - Ari Anguiano | [Github](https://github.com/crispulum) | [LinkedIn](https://www.linkedin.com/in/ari-anguiano)
68 | - Caleb Kao | [GitHub](https://github.com/caleb-kao) | [LinkedIn](https://www.linkedin.com/in/calebkao/)
69 | - Dinesh Wijesekara | [GitHub](https://github.com/Dwijesek) | [LinkedIn](https://www.linkedin.com/in/dinesh-wijesekara-a14b96251/)
70 | - Jonathan Kim | [GitHub](https://github.com/jonbingkim) | [LinkedIn](https://www.linkedin.com/in/jonbkim)
71 |
72 | ## License
73 |
74 | Distributed under the MIT License. See [LICENSE.txt](LICENSE.txt) for more information.
75 |
--------------------------------------------------------------------------------
/DEPLOYMENT.md:
--------------------------------------------------------------------------------
1 | # Deployment Guide
2 |
3 | ## AMI Automation with Packer
4 |
5 | Automate the creation of Amazon Machine Images (AMIs) using Packer to streamline your deployment process.
6 |
7 | ### Steps:
8 |
9 | 1. **Install Packer**
10 | Follow the installation instructions at [HashiCorp Packer](https://developer.hashicorp.com/packer/tutorials/aws-get-started/get-started-install-cli).
11 |
12 | 2. **Configure Packer Template**
13 | In `aws-ubuntu.pkr.hcl`, update the source within the build object and provisioner file to match the absolute path of the `dist` folder. Change `ami_name` to a unique value if you've used this file before (and change it each time).
14 |
15 | 3. **Initialize Packer**
16 | If this is your first time using this HCL file, run:
17 |
18 | ```
19 | packer init .
20 | ```
21 |
22 | 4. **Export AWS Credentials**
23 | Set your AWS credentials in the CLI environment:
24 |
25 | ```
26 | export AWS_ACCESS_KEY_ID=""
27 | export AWS_SECRET_ACCESS_KEY=""
28 | ```
29 |
30 | 5. **Create AMI**
31 | Execute:
32 | ```
33 | npm run packer
34 | ```
35 | The output will be the AMI image ID.
36 |
37 | **Alternative**
38 |
39 | If you do not wish to compile the image yourself, you can use this publicly-available version.
40 |
41 | ## Deployment Options
42 |
43 | ### Option 1: EC2
44 |
45 | For an automated and secure deployment option using EC2:
46 |
47 | 1. **Set Up AWS CLI**
48 | Install AWS CLI and configure it with your AWS credentials.
49 |
50 | 2. **Update YAML**
51 | Inside cloudFormationDeploy.yaml, update ImageId field with new AMI produced from Packer
52 |
53 | 3. **Install Dependencies**
54 | Run:
55 |
56 | ```
57 | npm install
58 | ```
59 |
60 | 4. **Deploy**
61 | Execute:
62 |
63 | ```
64 | npm run deploy
65 | ```
66 |
67 | This command creates and runs a CloudFormation stack, which sets up necessary security groups and deploys an EC2 instance with your server running. After all steps are completed, go to EC2 IPv4 address add port 3000 to the end and remove the “s” from “http” (e.g. http://ec2-3-144-128-131.us-east-2.compute.amazonaws.com:3000/ )
68 |
69 | **Alternative**
70 |
71 | If you do not wish to use access keys to login to the CLI, you can simply upload the CloudFormation Template yourself.
72 |
73 | **Note:** Ensure the stack name is unique each time you deploy by adjusting it in `package.json` under `npm run deploy`.
74 |
75 | ### Option 2: Fargate via ECR
76 |
77 | Deploy using ECS with Fargate for a scalable option:
78 |
79 | - **Advantages:** Easy updates roll-out.
80 | - **Disadvantages:** Setup time and complexity.
81 |
82 | **Security Group Details:**
83 | Allow HTTP/S traffic from your IP addresses.
84 |
85 | **IAM Role Details:**
86 | An EC2 Role permitting Lambda Full Access.
87 |
88 |
89 | ### Option 3: Local Deployment
90 |
91 | Quick and easy setup for local development:
92 |
93 | 1. Install dependencies:
94 | ```
95 | npm install
96 | ```
97 | 2. Start the application:
98 | ```
99 | npm run all
100 | ```
101 |
102 | Choose the option that best suits your deployment needs and follow the steps for a smooth and efficient setup.
103 |
--------------------------------------------------------------------------------
/src/server/controllers/dbController.ts:
--------------------------------------------------------------------------------
1 | import {PutItemCommand,GetItemCommand,DynamoDBClient} from "@aws-sdk/client-dynamodb";
2 | import dotenv from 'dotenv';
3 | import {Express, Request, Response, NextFunction} from 'express';
4 | import { marshall } from "@aws-sdk/util-dynamodb";
5 | import { LambdaClient } from "@aws-sdk/client-lambda"
6 |
7 |
8 |
9 |
10 | const DBClient = new DynamoDBClient();
11 |
12 |
13 | export async function getLambdaLogs(req: Request, res: Response, next: NextFunction): Promise {
14 |
15 | const { ARN } = req.body;
16 | if (ARN.length == 0){
17 | return next(new Error)
18 | }
19 | try
20 | {
21 | const query = {
22 | "Key": {
23 | "lambdaARN": {
24 | "S": ARN
25 | },
26 | },
27 | "TableName": "AWSPowerTuning"
28 | };
29 | const command = new GetItemCommand(query);
30 | const response = await DBClient.send(command);
31 | console.log(response,"RESPONSE FROM GETLAMBDALOGS")
32 | res.locals.output = response.Item
33 | return next()
34 | }
35 | catch(e){
36 | console.log(e)
37 | return next(e)
38 | }
39 | }
40 |
41 | export async function addLambdaLog (req: Request, res: Response, next: NextFunction): Promise {
42 | const {ARN, memoryArray, output, payload } = res.locals;
43 | // Add name above ^ for changes and change testFunction10 to use the variable
44 | if (!ARN.length || !memoryArray.length || !Object.values(output).length || !Object.values(payload).length){
45 | return next("Error in providing addLambdaLog params")
46 | }
47 | try
48 | {
49 | const item = {
50 | lambdaARN: ARN,
51 | functionName: "testFunction10",
52 | memoryArr: memoryArray,
53 | result: output,
54 | payload: payload
55 | };
56 |
57 | const q = {
58 | TableName: "AWSPowerTuning",
59 | Item: marshall(item)
60 | };
61 |
62 | const command = new PutItemCommand(q);
63 | const response = await DBClient.send(command);
64 |
65 | return next()
66 | }
67 | catch(e){
68 | if (e.name === 'ResourceNotFoundException') {
69 | //I think there's a region issue in addition to the 'what if there is not a table?' issue
70 | console.warn("DynamoDB table not found. Item not added.");
71 | return next();
72 | } else {
73 | console.error(e);
74 | return next();
75 | }
76 | }
77 |
78 | }
79 |
80 |
81 | /**
82 | * This code below is for getting the function body, still a WIP
83 | */
84 |
85 |
86 | // const lambdaClient = new LambdaClient(config);
87 | // const input = {
88 | // FunctionName: "arn:aws:lambda:us-west-1:066290895578:function:findPrime", // required
89 | // };
90 |
91 | // const getFuncDef = async (input) => {
92 | // const command = new GetFunctionCommand(input);
93 | // const response = await lambdaClient.send(command);
94 | // const presignedUrl = response.Code.Location;
95 |
96 | // https
97 | // .get(presignedUrl, (response) => {
98 | // const chunks = [];
99 |
100 | // response.on("data", (chunk) => {
101 | // console.log(chunk);
102 | // chunks.push(chunk);
103 | // });
104 |
105 | // response.on("end", () => {
106 | // const buffer = Buffer.concat(chunks);
107 | // const zip = new AdmZip(buffer);
108 | // const zipEntries = zip.getEntries();
109 |
110 | // zipEntries.forEach((zipEntry) => {
111 | // if (!zipEntry.isDirectory) {
112 |
113 | // const result = zipEntry.getData().toString("utf8")
114 | // return result
115 | // }
116 | // });
117 | // });
118 | // })
119 | // .on("error", (error) => {
120 | // console.error("Error: ", error);
121 | // });
122 | // };
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
--------------------------------------------------------------------------------
/src/components/graph.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | import './style.css'
3 | import { useEffect, useState } from "react";
4 | import { useSelector } from 'react-redux'
5 | import { RootState } from "../store";
6 | import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
7 |
8 |
9 | export default function Graph() {
10 | const graphData = useSelector((state: RootState) => state.results)
11 | const timeData = graphData.TimeData;
12 | const costData = graphData.CostData;
13 | const xData = graphData.MemoryData;
14 |
15 | //converts raw data into appropriate format for graph
16 | const convertData = () => {
17 | const temp = []
18 | for (let i = 0; i < xData.length; i++) {
19 | temp.push({
20 | 'Memory': xData[i],
21 | 'Invocation time': timeData[i],
22 | 'Runtime cost': costData[i]
23 | })
24 | }
25 | setData(temp)
26 | }
27 |
28 | const [data, setData] = useState([]);
29 |
30 | useEffect(() => {
31 | //setShow(false)
32 | convertData()
33 | }, [graphData])
34 |
35 | // Recharts code; largely self-explanatory
36 | return (
37 |
38 |
44 |
55 |
56 |
57 |
69 |
82 |
83 |
84 |
92 |
98 |
99 |
100 | {/* */}
101 |
102 | );
103 | };
104 |
105 |
106 |
--------------------------------------------------------------------------------
/src/components/graphDetailed.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | import './style.css'
3 | import { useEffect, useState } from "react";
4 | import { useSelector } from 'react-redux'
5 | import { RootState } from "../store";
6 | import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
7 |
8 |
9 | export default function GraphDetailed() {
10 | const graphData = useSelector((state: RootState) => state.results)
11 | const timeData = graphData.DetailedTimeData;
12 | const costData = graphData.DetailedCostData;
13 | const xData = graphData.DetailedMemoryData;
14 |
15 | //converts raw data into appropriate format for graph
16 | const convertData = () => {
17 | const temp = []
18 | for (let i = 0; i < xData.length; i++) {
19 | temp.push({
20 | 'Memory': xData[i],
21 | 'Invocation time': timeData[i],
22 | 'Runtime cost': costData[i]
23 | })
24 | }
25 | setData(temp)
26 | }
27 |
28 | const [data, setData] = useState([]);
29 |
30 | useEffect(() => {
31 | //setShow(false)
32 | convertData()
33 | }, [graphData])
34 |
35 | // Recharts code; largely self-explanatory
36 | return (
37 |
38 |
44 |
55 |
56 |
57 |
69 |
82 |
83 |
84 |
92 |
98 |
99 |
100 | {/* */}
101 |
102 | );
103 | };
104 |
105 |
106 |
--------------------------------------------------------------------------------
/src/scripts/cloudFormationDeploy.yaml:
--------------------------------------------------------------------------------
1 | AWSTemplateFormatVersion: 2010-09-09
2 | Description: EC2LambdaTestDeploy102
3 |
4 | Resources:
5 | MySecurityGroup:
6 | Type: "AWS::EC2::SecurityGroup"
7 | Properties:
8 | GroupDescription: Allow all traffic
9 | SecurityGroupIngress:
10 | - IpProtocol: -1
11 | FromPort: 0
12 | ToPort: 65535
13 | CidrIp: 0.0.0.0/0
14 | - IpProtocol: -1
15 | FromPort: 0
16 | ToPort: 65535
17 | CidrIpv6: "::/0"
18 |
19 | LambdaFullAccessRole:
20 | Type: "AWS::IAM::Role"
21 | Properties:
22 | AssumeRolePolicyDocument:
23 | Version: 2012-10-17
24 | Statement:
25 | - Effect: Allow
26 | Principal:
27 | Service:
28 | - ec2.amazonaws.com
29 | Action:
30 | - "sts:AssumeRole"
31 | Path: /
32 | Policies:
33 | - PolicyName: LambdaFullAccessPolicy
34 | PolicyDocument:
35 | Version: 2012-10-17
36 | Statement:
37 | - Effect: Allow
38 | Action: "lambda:*"
39 | Resource: "*"
40 | - Effect: Allow
41 | Action:
42 | - "ec2:DescribeAddresses"
43 | Resource: "*"
44 |
45 | ElasticIPManagementRole:
46 | Type: "AWS::IAM::Role"
47 | Properties:
48 | AssumeRolePolicyDocument:
49 | Version: 2012-10-17
50 | Statement:
51 | - Effect: Allow
52 | Principal:
53 | Service: ec2.amazonaws.com
54 | Action: "sts:AssumeRole"
55 | Path: /
56 | Policies:
57 | - PolicyName: ElasticIPManagementPolicy
58 | PolicyDocument:
59 | Version: 2012-10-17
60 | Statement:
61 | - Effect: Allow
62 | Action:
63 | - "ec2:AllocateAddress"
64 | - "ec2:AssociateAddress"
65 | - "ec2:DescribeAddresses"
66 | Resource: "*"
67 |
68 | EIP:
69 | Type: AWS::EC2::EIP
70 | Properties:
71 | Domain: vpc
72 |
73 | EC2InstanceProfile:
74 | Type: "AWS::IAM::InstanceProfile"
75 | Properties:
76 | Path: /
77 | Roles:
78 | - !Ref LambdaFullAccessRole
79 |
80 | Ec2Instance:
81 | Type: "AWS::EC2::Instance"
82 | Properties:
83 | InstanceType: t2.micro
84 | IamInstanceProfile: !Ref EC2InstanceProfile
85 | ImageId: ami-039f5d5f278d91a6f
86 | SecurityGroupIds:
87 | - !Ref MySecurityGroup
88 | UserData:
89 | Fn::Base64: !Sub |
90 | #!/bin/bash
91 | su - ubuntu -c "sudo -i -u ubuntu"
92 | su - ubuntu -c "curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.4/install.sh | bash"
93 | su - ubuntu -c "source ~/.nvm/nvm.sh && nvm install --lts"
94 | sudo apt-get update
95 | sudo apt-get install -y awscli nginx
96 | cd /home/ubuntu/dist
97 | npm install && npm run run-server-ec2
98 |
99 | cat << 'EOF' > /home/ubuntu/update_nginx.sh
100 | #!/bin/bash
101 | while true; do
102 | EIP=$(aws ec2 describe-addresses --region us-east-2 --filters "Name=instance-id,Values=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)" --query "Addresses[0].PublicIp" --output text)
103 | if [ -n "$EIP" ]; then
104 | echo "Elastic IP associated: $EIP"
105 | sudo tee /etc/nginx/sites-available/reverse-proxy </dev/null &
130 |
131 | EIPAssociation:
132 | Type: AWS::EC2::EIPAssociation
133 | DependsOn: Ec2Instance
134 | Properties:
135 | AllocationId: !GetAtt EIP.AllocationId
136 | InstanceId: !Ref Ec2Instance
137 |
--------------------------------------------------------------------------------
/src/components/chakraForm.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, FormEvent, useEffect, useState, ChangeEvent } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { RootState } from "../store";
4 | import { nameInput, arnInput, funcParamsInput, powerValueInput, testVolInput, checksInput } from "../formData/infoSlice";
5 | import { runOptimizer } from "../formData/resultsSlice";
6 | import * as ChakraUI from '@chakra-ui/react'
7 | import LoadingBar from "./loadingBar.tsx"
8 | import { Form, Field, useField, useForm } from "react-final-form";
9 | import validate from "./validate";
10 | import './style.css'
11 |
12 | const ChakraForm: React.FC = () => {
13 | const resultsState = useSelector((state: RootState) => state.results);
14 | const formState = useSelector((state: RootState) => state.info);
15 | const dispatch = useDispatch();
16 | const arnRef = useRef(null);
17 | const funcParamsRef = useRef(null);
18 | const memoryRef = useRef([]);
19 | const [show, setShow] = useState(false); //this is used to toggle whether the loading bar shows up
20 | const [checkedItems, setCheckedItems] = React.useState([true, false])
21 | // const toast = ChakraUI.useToast();
22 |
23 | useEffect(() => {
24 | if (formState.ARN !== '') {
25 | dispatch(runOptimizer(formState))
26 | }
27 | }, [formState])
28 |
29 | const onChangeName = (e: ChangeEvent) => {
30 | memoryRef.current[5] = e.target.value
31 | }
32 | const onChangeMinVal = (e: ChangeEvent) => {
33 | memoryRef.current[0] = e.target.value
34 | }
35 | const onChangeMaxVal = (e: ChangeEvent) => {
36 | memoryRef.current[1] = e.target.value
37 | }
38 |
39 | const onChangeIncrements = (e: ChangeEvent) => {
40 | memoryRef.current[2] = e.target.value
41 | }
42 |
43 | const onChangeTestVol = (e: ChangeEvent) => {
44 | memoryRef.current[3] = e.target.value
45 | }
46 |
47 |
48 |
49 | //onSubmit changes the form state then invokes post request to backend -JK
50 | const onSubmit = (e: FormEvent) => {
51 | e.preventDefault();
52 | console.log('submitted');
53 | setShow(true);
54 | dispatch(nameInput(memoryRef.current[5]))
55 | dispatch(arnInput(arnRef.current?.value || ''));
56 | dispatch(funcParamsInput(funcParamsRef.current?.value || ''));
57 | dispatch(powerValueInput(memoryRef.current));
58 | dispatch(testVolInput(memoryRef.current[3]))
59 | dispatch(checksInput(checkedItems))
60 | };
61 | useEffect(() => {
62 | setShow(false)
63 | }, [resultsState])
64 |
65 |
66 |
67 | return (
68 |
69 |
70 | }>
71 |
72 | ARN Details
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | Function Parameters
85 |
86 |
92 |
93 |
94 |
95 |
96 | Memory Allocation
97 | {/* Power Values */}
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
124 | Submit
125 |
126 |
127 | setCheckedItems([e.target.checked, checkedItems[1]])} fontSize='18px'>Fine-tuned Search
128 |
129 |
130 | setCheckedItems([checkedItems[0], e.target.checked])} isChecked={checkedItems[1]} fontSize='18px'>Concurrent Search
131 |
132 |
133 |
134 |
135 |
136 | );
137 | };
138 |
139 | export default ChakraForm;
--------------------------------------------------------------------------------
/src/server/controllers/lambdaController.ts:
--------------------------------------------------------------------------------
1 | import { myEventEmitter } from "../routes";
2 | import {
3 | LambdaClient,
4 | InvokeCommand,
5 | UpdateFunctionConfigurationCommand,
6 | } from "@aws-sdk/client-lambda";
7 |
8 | import { wait, extractBilledDurationFrom64, reduceObjectToMedian, calculateCosts, createCustomError, getRegionFromARN } from "../utils/utils.js"
9 |
10 | import { fromUtf8 } from "@aws-sdk/util-utf8-node";
11 |
12 |
13 | const lambdaController = {
14 | async shear(request, response, next) {
15 | // console.log("this works")
16 | if (!request.body.ARN) {
17 |
18 | const error = createCustomError('Error reading ARN!', 403, { body: request.body })
19 | return next(error);
20 | }
21 | if (!validateLambdaARN(request.body.ARN)) {
22 | console.log(request.body.ARN)
23 | const error = createCustomError('Invalid ARN!', 403, { body: request.body })
24 | return next(error);
25 | }
26 | const memoryArray = request.body.memoryArray;
27 | if (!memoryArray || !Array.isArray(memoryArray) || memoryArray.length === 0) {
28 | const error = createCustomError('Error with memory array!', 403, { body: request.body });
29 | return next(error);
30 | }
31 | const region2 = getRegionFromARN(request.body.ARN);
32 | const regionObj = { region: region2 }
33 | // setup for all the AWS work we're going to do.
34 | const lambdaClient = new LambdaClient(regionObj);
35 | response.locals.ARN = request.body.ARN
36 | response.locals.memoryArray = memoryArray;
37 | const TIMES = request.body.volume || 20
38 |
39 |
40 |
41 | const functionARN = request.body.ARN;
42 |
43 | const functionPayload = request.body.functionPayload;
44 | response.locals.payload = functionPayload
45 | const payloadBlob = fromUtf8(JSON.stringify(functionPayload));
46 |
47 | async function createNewVersionsFromMemoryArrayAndInvoke(inputArr, arn) {
48 | try {
49 | if (request.body.concurrent) {
50 | const outputObj = {};
51 |
52 | for (const element of inputArr) {
53 | const billedDurationArray = [];
54 | const invocations = [];
55 |
56 | const input = {
57 | FunctionName: arn,
58 | MemorySize: Number(element),
59 | Description: "New version with " + element + " MB of RAM"
60 | };
61 |
62 | const command = new UpdateFunctionConfigurationCommand(input);
63 | await lambdaClient.send(command);
64 | await wait(2000);
65 |
66 | for (let i = 0; i < TIMES; i++) {
67 | // Push all invocations into an array
68 | invocations.push(invokeSpecificVersion('$LATEST', payloadBlob));
69 | }
70 | await Promise.all(invocations)
71 |
72 | // Execute all invocations concurrently using Promise.all() await Promise.all(invocations);
73 |
74 | const invocations2 = [];
75 | for (let i = 0; i < TIMES; i++) {
76 | // Push all invocations into an array
77 | invocations2.push(invokeSpecificVersion('$LATEST', payloadBlob));
78 | }
79 | await Promise.all(invocations2)
80 | const invocations3 = [];
81 |
82 | for (let i = 0; i < TIMES; i++) {
83 | // Push all invocations into an array
84 | invocations3.push(invokeSpecificVersion('$LATEST', payloadBlob));
85 | }
86 | const results3 = await Promise.all(invocations3)
87 | billedDurationArray.push(...results3);
88 | outputObj[element] = billedDurationArray;
89 | console.log('concurrently invoked!')
90 | }
91 | return outputObj;
92 | }
93 | else {
94 |
95 | const outputObj = {}
96 |
97 | for (const element of inputArr) {
98 | const billedDurationArray = []
99 |
100 | const input = {
101 | FunctionName: arn,
102 | MemorySize:Number(element),
103 | Description: "New version with " + element +" MB of RAM"
104 | }
105 | const command = new UpdateFunctionConfigurationCommand(input)
106 | await lambdaClient.send(command)
107 |
108 | await wait(2000)
109 | outputObj[element] = billedDurationArray;
110 | for (let i = 0; i < TIMES; i++) {
111 | //invoke new version X times. currectly a global constant, but probably something we should let the user configure.
112 | const value = await invokeSpecificVersion('$LATEST', payloadBlob);
113 | billedDurationArray.push(value)
114 | }
115 |
116 | //await wait(2000);
117 | }
118 | console.log('sequentially invoked!')
119 | return outputObj;
120 |
121 | }
122 | } catch (error) {
123 | const customError = createCustomError('Error creating new versions.', 403, { body: request.body });
124 | return next(customError);
125 | }
126 | }
127 | async function invokeSpecificVersion(version, payload) {
128 | try {
129 | const invokeParams = {
130 | FunctionName: functionARN,
131 | Qualifier: version,
132 | Payload: payload,
133 |
134 | LogType: "Tail"
135 | };
136 | // @ts-expect-error - weird typing bug
137 | const data = await lambdaClient.send(new InvokeCommand(invokeParams));
138 |
139 | const billedDuration = extractBilledDurationFrom64(atob(data.LogResult))
140 | return billedDuration;
141 | } catch (error) {
142 | const customError = createCustomError('Error with invoking specific version.', 512, { body: request.body });
143 | return next(customError);
144 | }
145 | }
146 | try {
147 | const test = await createNewVersionsFromMemoryArrayAndInvoke(memoryArray, functionARN)
148 | const billedDurationArray = reduceObjectToMedian(test)
149 |
150 | const outputObject = {
151 | billedDurationOutput: billedDurationArray,
152 | costOutput: calculateCosts(billedDurationArray),
153 | bonusData: null
154 | }
155 | response.locals.output = outputObject;
156 | if (request.body.recursiveSearch) {
157 | //look through the best left and right...
158 | const entries = Object.entries(outputObject.costOutput)
159 | const minEntry = entries.reduce((min, entry) => (entry[1] < min[1] ? entry : min), entries[0]);
160 | const minEntryIndex = memoryArray.indexOf(Number(minEntry[0]))
161 |
162 | const midpoints: number[] = [];
163 | let first;
164 | let last;
165 | if (minEntryIndex === 0) {
166 | // Case where the first data point is the best cost/invocation
167 | first = 0;
168 | last = 2;
169 | for (let i = 1; i <= 7; i++) {
170 | const midpoint = Math.round((memoryArray[0] * (7 - i) + memoryArray[1] * i) / 7);
171 | midpoints.push(midpoint);
172 | }
173 | } else if (minEntryIndex === memoryArray.length - 1) {
174 | // Case where the last data point is the best cost/invocation
175 | first = minEntryIndex-1;
176 | last = minEntryIndex;
177 | for (let i = 1; i <= 7; i++) {
178 | const midpoint = Math.round((memoryArray[memoryArray.length - 2] * (7 - i) + memoryArray[memoryArray.length - 1] * i) / 7);
179 | midpoints.push(midpoint);
180 | }
181 | } else {
182 | // Normal case
183 | first = minEntryIndex - 1;
184 | last = minEntryIndex +1;
185 | for (let i = 1; i <= 7; i++) {
186 | const midpoint = Math.round((memoryArray[minEntryIndex - 1] * (7 - i) + memoryArray[minEntryIndex + 1] * i) / 7);
187 | midpoints.push(midpoint);
188 | }
189 | }
190 | midpoints.pop()
191 |
192 | const test2 = await createNewVersionsFromMemoryArrayAndInvoke(midpoints, functionARN)
193 |
194 | const billedDurationArray2 = reduceObjectToMedian(test2)
195 | //add first
196 | //add last
197 | billedDurationArray2[memoryArray[first]] = billedDurationArray[memoryArray[first]]
198 | billedDurationArray2[memoryArray[last]]= billedDurationArray[memoryArray[last]]
199 |
200 | const outputObject2 = {
201 | billedDurationOutput: billedDurationArray2,
202 | costOutput: calculateCosts(billedDurationArray2)
203 | }
204 | //console.log(outputObject2)
205 | outputObject.bonusData = outputObject2;
206 | }
207 | return next();
208 | }
209 | catch (error) {
210 | const customError = createCustomError('Unhandled error occurred.', 500, { body: request.body });
211 | return next(customError);
212 | }
213 | },
214 |
215 | };
216 |
217 | const lambdaArnRegex = /^arn:aws:lambda:[a-z\d-]+:\d{12}:function:[a-zA-Z0-9-_]+$/;
218 | function validateLambdaARN(arn) {
219 | return lambdaArnRegex.test(arn);
220 | }
221 |
222 |
223 |
224 |
225 | export default lambdaController;
226 |
227 |
228 |
229 |
--------------------------------------------------------------------------------