├── 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 | --------------------------------------------------------------------------------