diff --git a/.gitignore b/.gitignore index 93870c6..459a142 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,9 @@ .vs +__pycache__ bin obj -*.png \ No newline at end of file + +*.png +*.csv +*.jpg +*.mp4 \ No newline at end of file diff --git a/ASP.NET/Controllers/ImageController.cs b/ASP.NET/Controllers/ImageController.cs index ffbd67e..8c57ecf 100644 --- a/ASP.NET/Controllers/ImageController.cs +++ b/ASP.NET/Controllers/ImageController.cs @@ -23,6 +23,7 @@ namespace TCC.Controllers mstream.Position = 0; var result = ImageService.BoxBlurImage(mstream, radius); + mstream.Close(); var blurredImageStream = new MemoryStream(); result.Write(blurredImageStream); @@ -31,28 +32,16 @@ namespace TCC.Controllers return File(blurredImageStream, "image/png"); } - [HttpGet("load-image")] + [HttpGet("load-small-image")] public async Task GetSimpleImage() { - var result = ImageService.GetSimpleImage(); - - var imageStream = new MemoryStream(); - result.Write(imageStream); - imageStream.Position = 0; - - return File(imageStream, "image/png"); + return File(ImageService.GetSimpleImage(), "image/png"); } [HttpGet("load-big-image")] public async Task GetBigImage() { - var result = ImageService.GetBigImage(); - - var imageStream = new MemoryStream(); - result.Write(imageStream); - imageStream.Position = 0; - - return File(imageStream, "image/png"); + return File(ImageService.GetBigImage(), "image/png"); } [HttpPost("save-big-image")] @@ -61,6 +50,7 @@ namespace TCC.Controllers MemoryStream mstream = new MemoryStream(); await HttpContext.Request.Body.CopyToAsync(mstream); mstream.Position = 0; + mstream.Close(); return Ok(); } diff --git a/ASP.NET/Dockerfile b/ASP.NET/Dockerfile index 8380598..b1d57a5 100644 --- a/ASP.NET/Dockerfile +++ b/ASP.NET/Dockerfile @@ -2,6 +2,7 @@ FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim AS build-env WORKDIR /App # Copy everything +RUN apt update && apt install wget -y COPY * . # Restore as distinct layers @@ -10,15 +11,21 @@ RUN dotnet restore # Build a release RUN dotnet build -c Release -o out +RUN cd out && \ + wget https://files.ivanch.me/api/public/dl/Dj0gkp-m/small-image.png && \ + wget https://files.ivanch.me/api/public/dl/FqHEPM1Q/big-image.png && \ + wget https://files.ivanch.me/api/public/dl/nTAYqZwD/video.mp4 && \ + rm -rf runtimes && \ + mkdir -p ./static && \ + mv small-image.png ./static && \ + mv big-image.png ./static && \ + mv video.mp4 ./static + # Build runtime image FROM mcr.microsoft.com/dotnet/aspnet:6.0-bullseye-slim WORKDIR /App -RUN wget https://files.ivanch.me/api/public/dl/QFCLgtrG/simpleimage.png && \ - wget https://files.ivanch.me/api/public/dl/E0VLgWbx/bigimage.png && \ - rm -rf runtimes - COPY --from=build-env /App/out . ENTRYPOINT ["dotnet", "/App/TCC.APP.dll"] \ No newline at end of file diff --git a/ASP.NET/ImageHelper.cs b/ASP.NET/ImageHelper.cs index 78aece5..df0b189 100644 --- a/ASP.NET/ImageHelper.cs +++ b/ASP.NET/ImageHelper.cs @@ -4,13 +4,13 @@ namespace tcc_app { public static class ImageHelper { - public static MagickImage SimpleImage; - public static MagickImage BigImage; + public static byte[] SimpleImage; + public static byte[] BigImage; static ImageHelper() { - SimpleImage = new MagickImage("simpleimage.png"); - BigImage = new MagickImage("bigimage.png"); + SimpleImage = File.ReadAllBytes("static/small-image.png"); + BigImage = File.ReadAllBytes("static/big-image.png"); } } } diff --git a/ASP.NET/Program.cs b/ASP.NET/Program.cs index 1a6b4d7..fdbd501 100644 --- a/ASP.NET/Program.cs +++ b/ASP.NET/Program.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.FileProviders; using TCC.Services; namespace TCC @@ -18,9 +19,13 @@ namespace TCC options.Limits.MaxRequestBodySize = int.MaxValue; // if don't set default value is: 30 MB }); - var app = builder.Build(); + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = new PhysicalFileProvider(Path.Combine(builder.Environment.ContentRootPath, "static")), + RequestPath = "/static" + }); app.MapControllers(); app.Run(); diff --git a/ASP.NET/Properties/launchSettings.json b/ASP.NET/Properties/launchSettings.json index c972bd3..5b073a0 100644 --- a/ASP.NET/Properties/launchSettings.json +++ b/ASP.NET/Properties/launchSettings.json @@ -14,7 +14,7 @@ "dotnetRunMessages": true, "launchBrowser": false, "launchUrl": "weatherforecast", - "applicationUrl": "http://0.0.0.0:5100", + "applicationUrl": "http://0.0.0.0:9090", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/ASP.NET/Services/ImageService.cs b/ASP.NET/Services/ImageService.cs index fbe3d02..99e2861 100644 --- a/ASP.NET/Services/ImageService.cs +++ b/ASP.NET/Services/ImageService.cs @@ -62,14 +62,16 @@ namespace TCC.Services file.Close(); } - public MagickImage GetSimpleImage() + public byte[] GetSimpleImage() { - return ImageHelper.SimpleImage; + return File.ReadAllBytes("static/small-image.png"); + //return ImageHelper.SimpleImage; } - public MagickImage GetBigImage() + public byte[] GetBigImage() { - return ImageHelper.BigImage; + return File.ReadAllBytes("static/big-image.png"); + //return ImageHelper.BigImage; } } } diff --git a/ASP.NET/static/nginx.html b/ASP.NET/static/nginx.html new file mode 100644 index 0000000..0ceac88 --- /dev/null +++ b/ASP.NET/static/nginx.html @@ -0,0 +1,33 @@ + + + + Welcome to nginx! + + + +

Welcome to nginx!

+

+ If you see this page, the nginx web server is successfully installed and + working. Further configuration is required. +

+ +

+ For online documentation and support please refer to + nginx.org.
+ Commercial support is available at + nginx.com. +

+ +

Thank you for using nginx.

+ + \ No newline at end of file diff --git a/scripts/graph.py b/scripts/graph.py new file mode 100644 index 0000000..6a82475 --- /dev/null +++ b/scripts/graph.py @@ -0,0 +1,89 @@ +import numpy as np +import matplotlib.pyplot as plt + +def plot_graph(x, y, title, x_label, y_label, filename): + plt.plot(x, y, 'ro', markersize=1, linewidth=0.5, linestyle='solid') + plt.title(title) + plt.xlabel(x_label) + plt.ylabel(y_label) + plt.savefig(f'{filename}.png') + + plt.clf() + plt.close('all') + +def plot_resource_graph(x_data, y_data, title, x_label, y_label, filename): + requests = x_data + resource = { + 'CPU': [p[0] for p in y_data], + 'RAM': [p[1] for p in y_data], + } + + x = np.arange(len(requests)) + width = 0.25 + multiplier = 0 + + fig, ax = plt.subplots(layout='constrained') + + for attribute, measurement in resource.items(): + offset = width * multiplier + + rects = ax.bar(x + offset, measurement, width, label=attribute) + ax.bar_label(rects, padding=3) + multiplier += 1 + + # Add some text for labels, title and custom x-axis tick labels, etc. + ax.set_xlabel(x_label) + ax.set_ylabel(y_label) + ax.set_title(title) + ax.set_xticks(x + (width/2), requests) + ax.legend(loc='upper left', ncols=len(resource.items())) + ax.set_ylim(0, 100) + + plt.savefig(f'{filename}.png') + + plt.clf() + plt.close('all') + +def get_data(filename): + lines = [] + with open(filename, 'r') as f: + lines = f.readlines() + + x = [] + y = [] + for line in lines: + line = line.strip().split(',') + if line: + x.append(int(line[0])) + y.append(float(line[1])) + + return x, y + +def get_resource_data(filename): + lines = [] + with open(filename, 'r') as f: + lines = f.readlines() + + x = [] + y = [] + for line in lines: + line = line.strip().split(',') + if line: + x.append(int(line[0])) # requests + y.append([float(v)*100 for v in line[1:]]) # cpu, ram + + return x, y + +def generate_req_graph(filename, framework_name, endpoint_name): + x, y = get_data(filename) + + filename = filename.split('/')[-1] + new_filename = filename.replace('.csv', '') + plot_graph(x, y, f'{framework_name} - {endpoint_name}', 'Número de requisições', 'Requisições por segundo', new_filename) + +def generate_resource_graph(filename, framework_name, endpoint_name): + x, y = get_resource_data(filename) + + filename = filename.split('/')[-1] + new_filename = filename.replace('.csv', '') + plot_resource_graph(x, y, f'{framework_name} - {endpoint_name}', 'Uso de recursos', 'Uso (%)', new_filename) \ No newline at end of file diff --git a/scripts/init.py b/scripts/init.py new file mode 100644 index 0000000..871b2e8 --- /dev/null +++ b/scripts/init.py @@ -0,0 +1,17 @@ +import requests + +def download_file(url): + local_filename = url.split('/')[-1] + with requests.get(url, stream=True) as r: + r.raise_for_status() + with open(local_filename, 'wb') as f: + for chunk in r.iter_content(chunk_size=8192): + # If you have chunk encoded response uncomment if + # and set chunk_size parameter to None. + #if chunk: + f.write(chunk) + return local_filename + +def init(): + download_file('https://files.ivanch.me/api/public/dl/Dj0gkp-m/small-image.png') + download_file('https://files.ivanch.me/api/public/dl/FqHEPM1Q/big-image.png') \ No newline at end of file diff --git a/scripts/testes.py b/scripts/testes.py index d0eb763..10eb847 100644 --- a/scripts/testes.py +++ b/scripts/testes.py @@ -1,33 +1,155 @@ import requests +import docker import concurrent.futures import time +import sys +import os +from graph import generate_req_graph, generate_resource_graph +from math import floor +from init import init -URL_BASE = 'http://localhost:5100' +if len(sys.argv) < 2 or len(sys.argv) > 3 or sys.argv[1] == '-h' or sys.argv[1] == '--help': + print("Usage: python testes.py [container name]") + sys.exit(1) -num_requests = [1000, 5000, 10_000, 50_000, 100_000, 500_000, 1_000_000] +init() -def send_request(session, url): - response = session.get(url) - return response.status_code +THREADS = 10 +FRAMEWORK_NAME = sys.argv[1] +CONTAINER_NAME = sys.argv[2] if len(sys.argv) > 2 else "" +URL_BASE = 'http://localhost:9090' +BLUR_RADIUS = 5 +API_REQUESTS = [ + ('/image/save-big-image', 'POST', range(0, 10_000, 1_000), open('big-image.png', 'rb').read()), + (f'/image/blur?radius={BLUR_RADIUS}', 'POST', range(0, 1_000, 50), open('small-image.png', 'rb').read()), + ('/status/ok', 'GET', range(0, 30_000, 5000), None), + ('/image/load-small-image', 'GET', range(0, 30_000, 5000), None), + ('/static/small-image.png', 'GET', range(0, 30_000, 5000), None), + ('/image/load-big-image', 'GET', range(0, 500, 50), None), + ('/static/big-image.png', 'GET', range(0, 500, 50), None), + ('/static/video.mp4', 'GET', range(0, 10_000, 1_000), None), +] + +def send_request(url, method = 'GET', payload = None): + success = False + responses = { + 2: 0, # OK + 4: 0, # Bad Request + 5: 0, # Server Error + } + while not success: + try: + response = None + if method == 'GET': + response = requests.get(url) + elif method == 'POST': + response = requests.post(url, data=payload, headers={'Content-Type': 'image/png'}) + except: + continue + success = response.status_code == 200 + responses[floor(response.status_code/100)] += 1 + return responses + +def getFileNames(endpoint): + endpoint = endpoint.replace('/', '') + + files = [ + f"data/req_{FRAMEWORK_NAME}_{endpoint}.csv", + f"data/resource_{FRAMEWORK_NAME}_{endpoint}.csv", + ] + + return files + +def record(filename, requests, reqpersec): + with open(filename, "a") as file: + file.write(f"{requests},{reqpersec}\n") + +def record_resource(filename, requests, cpu, ram): + with open(filename, "a") as file: + file.write(f"{requests},{cpu},{ram}\n") + +def run_tests(endpoint, method, num_requests, metadata): + files = getFileNames(endpoint) + for filename in files: + if os.path.exists(filename): + os.remove(filename) -def main(): for num_request in num_requests: - with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: - url = f'{URL_BASE}/status/ok' + if num_request <= 0: continue + + ok_responses = 0 + bad_responses = 0 + server_errors = 0 + cpu, ram = 0, 0 + + with concurrent.futures.ThreadPoolExecutor(max_workers=THREADS) as executor: + url = f'{URL_BASE}{endpoint}' start_time = time.time() futures = [] - with requests.Session() as session: - futures = [executor.submit(send_request, session, url) for _ in range(num_request)] + #with requests.Session() as session: + # futures = [executor.submit(send_request, session, url) for _ in range(num_request)] + + half = floor(num_request/2) + for i in range(num_request): + futures.append(executor.submit(send_request, url, method, metadata)) + + if i == half: + cpu, ram = get_resource_usage() concurrent.futures.wait(futures) elapsed_time = time.time() - start_time - successful_responses = sum(1 for future in futures if future.result() == 200) + for future in futures: + responses = future.result() + ok_responses += responses[2] + bad_responses += responses[4] + server_errors += responses[5] - print(f"All requests completed in {elapsed_time:.2f} seconds. {elapsed_time/num_request:.4f} seconds per request. {num_request/elapsed_time:.2f} requests per second.") - print(f"Successful responses: {successful_responses}/{num_request}") + print(f"{num_request}: {elapsed_time:.2f} seconds. {elapsed_time/num_request:.4f} seconds per request. {num_request/elapsed_time:.2f} requests per second. [OK: {ok_responses}, Bad Request: {bad_responses}, Server Error: {server_errors}]]") + record(files[0], num_request, f"{num_request/elapsed_time:.2f}") + record_resource(files[1], num_request, cpu, ram) -main() \ No newline at end of file + generate_req_graph(files[0], FRAMEWORK_NAME, endpoint) + generate_resource_graph(files[1], FRAMEWORK_NAME, endpoint) + + time.sleep(3) + +def get_resource_usage(): + if CONTAINER_NAME == "": return 0, 0 + + try: + client = docker.from_env() + stats = client.containers.get(CONTAINER_NAME).stats(stream=False) + except: + return 0, 0 # unable to get stats + + return get_cpu_usage(stats), get_ram_usage(stats) + +def get_cpu_usage(stats): + UsageDelta = stats['cpu_stats']['cpu_usage']['total_usage'] - stats['precpu_stats']['cpu_usage']['total_usage'] + SystemDelta = stats['cpu_stats']['system_cpu_usage'] - stats['precpu_stats']['system_cpu_usage'] + len_cpu = stats['cpu_stats']['online_cpus'] + percentage = (UsageDelta / SystemDelta) * len_cpu + return f"{percentage:.2f}" + +def get_ram_usage(stats): + usage = stats['memory_stats']['usage'] + limit = stats['memory_stats']['limit'] + + percentage = (usage / limit) + + # percent = round(percentage, 2) + return f"{percentage:.2f}" + +if __name__ == "__main__": + if not os.path.exists("data"): + os.mkdir("data") + else: + os.system("rm -rf data/*") + + for endpoint, method, num_requests, metadata in API_REQUESTS: + print(f"# {endpoint}") + run_tests(endpoint, method, num_requests, metadata)