Compare commits

...

17 Commits

37 changed files with 2086 additions and 271 deletions

10
.gitignore vendored
View File

@@ -1,4 +1,14 @@
.vs .vs
__pycache__
bin bin
obj obj
*.vscode
__pycache__
.idea
*/target
*.png *.png
*.csv
*.jpg
*.mp4

View File

@@ -23,6 +23,7 @@ namespace TCC.Controllers
mstream.Position = 0; mstream.Position = 0;
var result = ImageService.BoxBlurImage(mstream, radius); var result = ImageService.BoxBlurImage(mstream, radius);
mstream.Close();
var blurredImageStream = new MemoryStream(); var blurredImageStream = new MemoryStream();
result.Write(blurredImageStream); result.Write(blurredImageStream);
@@ -31,28 +32,16 @@ namespace TCC.Controllers
return File(blurredImageStream, "image/png"); return File(blurredImageStream, "image/png");
} }
[HttpGet("load-image")] [HttpGet("load-small-image")]
public async Task<IActionResult> GetSimpleImage() public async Task<IActionResult> GetSimpleImage()
{ {
var result = ImageService.GetSimpleImage(); return File(ImageService.GetSimpleImage(), "image/png");
var imageStream = new MemoryStream();
result.Write(imageStream);
imageStream.Position = 0;
return File(imageStream, "image/png");
} }
[HttpGet("load-big-image")] [HttpGet("load-big-image")]
public async Task<IActionResult> GetBigImage() public async Task<IActionResult> GetBigImage()
{ {
var result = ImageService.GetBigImage(); return File(ImageService.GetBigImage(), "image/png");
var imageStream = new MemoryStream();
result.Write(imageStream);
imageStream.Position = 0;
return File(imageStream, "image/png");
} }
[HttpPost("save-big-image")] [HttpPost("save-big-image")]
@@ -62,6 +51,9 @@ namespace TCC.Controllers
await HttpContext.Request.Body.CopyToAsync(mstream); await HttpContext.Request.Body.CopyToAsync(mstream);
mstream.Position = 0; mstream.Position = 0;
ImageService.SaveImage(mstream);
mstream.Close();
return Ok(); return Ok();
} }
} }

View File

@@ -2,6 +2,7 @@ FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim AS build-env
WORKDIR /App WORKDIR /App
# Copy everything # Copy everything
RUN apt update && apt install wget -y
COPY * . COPY * .
# Restore as distinct layers # Restore as distinct layers
@@ -10,15 +11,21 @@ RUN dotnet restore
# Build a release # Build a release
RUN dotnet build -c Release -o out RUN dotnet build -c Release -o out
RUN cd out && \
wget https://files.ivanch.me/api/public/dl/iFuXSNhw/small-image.png && \
wget https://files.ivanch.me/api/public/dl/81Bkht5C/big-image.png && \
wget https://files.ivanch.me/api/public/dl/nAndfAjK/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 # Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:6.0-bullseye-slim FROM mcr.microsoft.com/dotnet/aspnet:6.0-bullseye-slim
WORKDIR /App 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 . COPY --from=build-env /App/out .
ENTRYPOINT ["dotnet", "/App/TCC.APP.dll"] ENTRYPOINT ["dotnet", "/App/TCC.APP.dll"]

View File

@@ -1,16 +0,0 @@
using ImageMagick;
namespace tcc_app
{
public static class ImageHelper
{
public static MagickImage SimpleImage;
public static MagickImage BigImage;
static ImageHelper()
{
SimpleImage = new MagickImage("simpleimage.png");
BigImage = new MagickImage("bigimage.png");
}
}
}

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.FileProviders;
using TCC.Services; using TCC.Services;
namespace TCC namespace TCC
@@ -18,9 +19,13 @@ namespace TCC
options.Limits.MaxRequestBodySize = int.MaxValue; // if don't set default value is: 30 MB options.Limits.MaxRequestBodySize = int.MaxValue; // if don't set default value is: 30 MB
}); });
var app = builder.Build(); var app = builder.Build();
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(builder.Environment.ContentRootPath, "static")),
RequestPath = "/static"
});
app.MapControllers(); app.MapControllers();
app.Run(); app.Run();

View File

@@ -14,7 +14,7 @@
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": false, "launchBrowser": false,
"launchUrl": "weatherforecast", "launchUrl": "weatherforecast",
"applicationUrl": "http://0.0.0.0:5100", "applicationUrl": "http://0.0.0.0:9090",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }

View File

@@ -1,5 +1,4 @@
using ImageMagick; using ImageMagick;
using tcc_app;
namespace TCC.Services namespace TCC.Services
{ {
@@ -10,11 +9,13 @@ namespace TCC.Services
public MagickImage BoxBlurImage(Stream imageStream, int radius) public MagickImage BoxBlurImage(Stream imageStream, int radius)
{ {
var image = new MagickImage(imageStream); var image = new MagickImage(imageStream);
var blurredImage = new MagickImage(image); image.GaussianBlur(radius, radius);
return image;
//var blurredImage = new MagickImage(image);
blurredImage = BoxBlurImageSeparable(image, blurredImage, radius, 0); //blurredImage = BoxBlurImageSeparable(image, blurredImage, radius, 0);
blurredImage = BoxBlurImageSeparable(image, blurredImage, 0, radius); //blurredImage = BoxBlurImageSeparable(blurredImage, blurredImage, 0, radius);
return blurredImage; //return blurredImage;
} }
private MagickImage BoxBlurImageSeparable(MagickImage image, MagickImage blurredImage, int radiusX, int radiusY) private MagickImage BoxBlurImageSeparable(MagickImage image, MagickImage blurredImage, int radiusX, int radiusY)
@@ -62,14 +63,14 @@ namespace TCC.Services
file.Close(); file.Close();
} }
public MagickImage GetSimpleImage() public byte[] GetSimpleImage()
{ {
return ImageHelper.SimpleImage; return File.ReadAllBytes("static/small-image.png");
} }
public MagickImage GetBigImage() public byte[] GetBigImage()
{ {
return ImageHelper.BigImage; return File.ReadAllBytes("static/big-image.png");
} }
} }
} }

33
ASP.NET/static/nginx.html Normal file
View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html {
color-scheme: light dark;
}
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>
If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.
</p>
<p>
For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br />
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.
</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>

1482
ActixAPI/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

11
ActixAPI/Cargo.toml Normal file
View File

@@ -0,0 +1,11 @@
[package]
name = "ActixAPI"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web = "4"
actix-files = "0.6.2"
magick_rust = "0.19.0"

25
ActixAPI/Dockerfile Normal file
View File

@@ -0,0 +1,25 @@
FROM rust:slim-bullseye AS build-env
WORKDIR /app
COPY . .
RUN apt update && apt install wget -y && \
wget https://files.ivanch.me/api/public/dl/iFuXSNhw/small-image.png && \
wget https://files.ivanch.me/api/public/dl/81Bkht5C/big-image.png && \
wget https://files.ivanch.me/api/public/dl/nAndfAjK/video.mp4 && \
mv small-image.png ./static && \
mv big-image.png ./static && \
mv video.mp4 ./static
RUN cargo build --release
FROM debian:bullseye-slim
WORKDIR /app
COPY --from=build-env /app/target/release .
COPY --from=build-env /app/static ./static
ENTRYPOINT ["./ActixAPI"]

58
ActixAPI/src/main.rs Normal file
View File

@@ -0,0 +1,58 @@
use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder, HttpRequest, Result};
use actix_files::NamedFile;
use magick_rust::{MagickWand, ToMagick};
use magick_rust::{self, FilterType};
#[get("/status/ok")]
async fn hello() -> impl Responder {
HttpResponse::Ok().body("{\"status\": 200}")
}
#[post("/echo")]
async fn echo(req_body: String) -> impl Responder {
HttpResponse::Ok().body(req_body)
}
async fn manual_hello() -> impl Responder {
HttpResponse::Ok().body("Hey there!")
}
async fn static_serve(req: HttpRequest) -> Result<NamedFile> {
let path: &str = req.path();
let real_path = &path[1..];
Ok(NamedFile::open(real_path)?)
}
#[post("/image/blur")]
async fn blur_image(image_data: web::Bytes) -> Result<HttpResponse> {
// Load the image from the incoming bytes
let image = ToMagick::<magick_rust::MagickWand>::to_magick(image_data).unwrap();
// Apply a blur filter to the image
let blurred_image = image.blur(FilterType::Gaussian, 0.0).unwrap();
// Convert the blurred image back to bytes
let blurred_image_bytes = blurred_image.to_bytes().unwrap();
Ok(HttpResponse::Ok()
.content_type("image/jpeg")
.body(blurred_image_bytes))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
println!("Hello, world!");
HttpServer::new(|| {
App::new()
.route("/static/{filename:.*}", web::get().to(static_serve))
.service(hello)
.service(echo)
.service(blur_image)
.route("/hey", web::get().to(manual_hello))
})
.bind(("0.0.0.0", 9090))?
.run()
.await
}

View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html {
color-scheme: light dark;
}
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>
If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.
</p>
<p>
For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br />
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.
</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>

8
FlaskAPI/.idea/.gitignore generated vendored
View File

@@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="Flask">
<option name="enabled" value="true" />
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/../FlaskAPI\templates" />
</list>
</option>
</component>
</module>

View File

@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (FlaskAPI)" project-jdk-type="Python SDK" />
</project>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/FlaskAPI.iml" filepath="$PROJECT_DIR$/.idea/FlaskAPI.iml" />
</modules>
</component>
</project>

View File

@@ -1,17 +0,0 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "Python: Flask",
"type": "python",
"request": "launch",
"module": "flask",
"env": { "FLASK_APP": "app.py", "FLASK_DEBUG": "1" },
"args": ["run", "--no-debugger", "--no-reload"],
"jinja": true,
"justMyCode": true
}
]
}

View File

View File

@@ -1,24 +1,11 @@
# syntax=docker/dockerfile:1
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/engine/reference/builder/
ARG PYTHON_VERSION=3.10.12 ARG PYTHON_VERSION=3.10.12
FROM python:${PYTHON_VERSION}-slim as base FROM python:${PYTHON_VERSION}-slim as base
# Prevents Python from writing pyc files.
ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONDONTWRITEBYTECODE=1
# Keeps Python from buffering stdout and stderr to avoid situations where
# the application crashes without emitting any logs due to buffering.
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
WORKDIR /app WORKDIR /app
# Copy the source code into the container.
COPY . .
RUN apt-get update && apt-get install -y imagemagick && apt-get install -y wget && ls RUN apt-get update && apt-get install -y imagemagick && apt-get install -y wget && ls
RUN wget https://files.ivanch.me/api/public/dl/iFuXSNhw/small-image.png && \ RUN wget https://files.ivanch.me/api/public/dl/iFuXSNhw/small-image.png && \
@@ -30,16 +17,13 @@ RUN wget https://files.ivanch.me/api/public/dl/iFuXSNhw/small-image.png && \
mv big-image.png ./static && \ mv big-image.png ./static && \
mv video.mp4 ./static mv video.mp4 ./static
# Download dependencies as a separate step to take advantage of Docker's caching. COPY . .
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
# Leverage a bind mount to requirements.txt to avoid having to copy them into
# into this layer.
RUN --mount=type=cache,target=/root/.cache/pip \ RUN --mount=type=cache,target=/root/.cache/pip \
--mount=type=bind,source=requirements.txt,target=requirements.txt \ --mount=type=bind,source=requirements.txt,target=requirements.txt \
python -m pip install -r requirements.txt python -m pip install -r requirements.txt
# Expose the port that the application listens on.
EXPOSE 5000 EXPOSE 5000
# Run the application. # Run the application.
CMD gunicorn 'app:app' --bind=0.0.0.0:5000 CMD gunicorn 'app:app' --bind=0.0.0.0:5000 --timeout 3600

View File

@@ -4,9 +4,9 @@ from controllers.image import image_blueprint
app = Flask(__name__) app = Flask(__name__)
if __name__ == '__main__':
app.run()
# #
app.register_blueprint(status_blueprint) app.register_blueprint(status_blueprint)
app.register_blueprint(image_blueprint) app.register_blueprint(image_blueprint)
if __name__ == '__main__':
app.run()

View File

@@ -1,49 +0,0 @@
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Docker compose reference guide at
# https://docs.docker.com/compose/compose-file/
# Here the instructions define your application as a service called "server".
# This service is built from the Dockerfile in the current directory.
# You can add other services your application may depend on here, such as a
# database or a cache. For examples, see the Awesome Compose repository:
# https://github.com/docker/awesome-compose
services:
server:
build:
context: .
ports:
- 5000:5000
# The commented out section below is an example of how to define a PostgreSQL
# database that your application can use. `depends_on` tells Docker Compose to
# start the database before your application. The `db-data` volume persists the
# database data between container restarts. The `db-password` secret is used
# to set the database password. You must create `db/password.txt` and add
# a password of your choosing to it before running `docker compose up`.
# depends_on:
# db:
# condition: service_healthy
# db:
# image: postgres
# restart: always
# user: postgres
# secrets:
# - db-password
# volumes:
# - db-data:/var/lib/postgresql/data
# environment:
# - POSTGRES_DB=example
# - POSTGRES_PASSWORD_FILE=/run/secrets/db-password
# expose:
# - 5432
# healthcheck:
# test: [ "CMD", "pg_isready" ]
# interval: 10s
# timeout: 5s
# retries: 5
# volumes:
# db-data:
# secrets:
# db-password:
# file: db/password.txt

View File

@@ -1,28 +1,27 @@
import io
from services.image import ImageService from services.image import ImageService
from static.image_helper import ImageHelper
from flask import request, Response, Blueprint, jsonify, send_file from flask import request, Response, Blueprint, jsonify, send_file
image_blueprint = Blueprint('image_blueprint', __name__) image_blueprint = Blueprint('image_blueprint', __name__)
image_service = ImageService() image_service = ImageService()
@image_blueprint.route('/image/blur', methods=['POST']) @image_blueprint.route('/image/blur', methods=['POST'])
def blur_image(): def blur_image():
radius = int(request.form.get('radius')) radius = int(request.args.get('radius'))
image = request.files.get('file') image = request.get_data()
if radius and image: if radius and image:
return send_file(image_service.box_blur_image(image, radius), return send_file(io.BytesIO(image_service.box_blur_image(image, radius)),
mimetype='image/jpeg', mimetype='image/png',
as_attachment=True, as_attachment=True,
download_name='blurred_image.jpeg') download_name='blurred_image.png')
return "Bad request", 400 return "Bad request", 400
@image_blueprint.route('/image/load-small-image', methods=['GET']) @image_blueprint.route('/image/load-small-image', methods=['GET'])
def get_simple_image(): def get_simple_image():
return send_file(image_service.get_simple_image(), return send_file(io.BytesIO(image_service.get_simple_image()),
mimetype='image/png', mimetype='image/png',
as_attachment=True, as_attachment=True,
download_name='small-image.png') download_name='small-image.png')
@@ -30,7 +29,7 @@ def get_simple_image():
@image_blueprint.route('/image/load-big-image', methods=['GET']) @image_blueprint.route('/image/load-big-image', methods=['GET'])
def get_big_image(): def get_big_image():
return send_file(image_service.get_big_image(), return send_file(io.BytesIO(image_service.get_big_image()),
mimetype='image/png', mimetype='image/png',
as_attachment=True, as_attachment=True,
download_name='big-image.png') download_name='big-image.png')
@@ -38,4 +37,6 @@ def get_big_image():
@image_blueprint.route('/image/save-big-image', methods=['POST']) @image_blueprint.route('/image/save-big-image', methods=['POST'])
def save_image(): def save_image():
pass image_service.save_image(request.get_data())
return "OK", 200

View File

@@ -1,69 +1,60 @@
from wand.image import Image from wand.image import Image, Color
from static.image_helper import ImageHelper def box_blur_image_separable(image, radius_x, radius_y):
width, height = image.width, image.height
kernel_x_size = 2 * radius_x + 1
kernel_y_size = 2 * radius_y + 1
kernel_area = kernel_x_size * kernel_y_size
blurred_image = image.clone()
def box_blur_image_separable(image, blurred_image, radius_x, radius_y): for y in range(height):
pixels = image.get_pixels() for x in range(width):
blurred_pixels = blurred_image.get_pixels()
for pixel in pixels:
x, y = pixel[0], pixel[1]
r_total, g_total, b_total = 0, 0, 0 r_total, g_total, b_total = 0, 0, 0
pixel_count = 0
for offset_y in range(-radius_y, radius_y + 1): for offset_y in range(-radius_y, radius_y + 1):
for offset_x in range(-radius_x, radius_x + 1): for offset_x in range(-radius_x, radius_x + 1):
new_x = x + offset_x new_x = x + offset_x
new_y = y + offset_y new_y = y + offset_y
if 0 <= new_x < image.width and 0 <= new_y < image.height: if 0 <= new_x < width and 0 <= new_y < height:
pixel_color = pixels[new_y * image.width + new_x] pixel = image[new_x, new_y]
r_total += pixel_color.red r_total += pixel.red_int8
g_total += pixel_color.green g_total += pixel.green_int8
b_total += pixel_color.blue b_total += pixel.blue_int8
pixel_count += 1
blurred_pixel = blurred_pixels[y * image.width + x] r_avg = int(r_total / kernel_area)
blurred_pixel.red = int(r_total / pixel_count) g_avg = int(g_total / kernel_area)
blurred_pixel.green = int(g_total / pixel_count) b_avg = int(b_total / kernel_area)
blurred_pixel.blue = int(b_total / pixel_count)
blurred_image[x, y] = Color(f'rgb({r_avg},{g_avg},{b_avg})')
return blurred_image return blurred_image
def save_image(file_stream):
with open("image.png", "wb") as file:
file.write(file_stream.read())
file.close()
class ImageService: class ImageService:
def __init__(self): def __init__(self):
pass pass
def box_blur_image(self, img, radius): def box_blur_image(self, img, radius):
temp_path = 'temp_image.png' with Image(blob=img) as image:
img.save(temp_path) image.gaussian_blur(radius, radius)
return image.make_blob()
with Image(filename=temp_path) as img:
img.blur(radius, 2)
blurred_temp_path = 'blurred_temp_image.png'
img.save(filename='blurred_temp_image.png')
return blurred_temp_path
# blurred_image = box_blur_image_separable(image, radius, 0)
# blurred_image = box_blur_image_separable(blurred_image, 0, radius)
# return blurred_image.make_blob()
def get_simple_image(self): def get_simple_image(self):
with ImageHelper.SimpleImage as img: with open("./static/small-image.png", "rb") as file:
img = ImageHelper.SimpleImage return file.read()
simple_image = 'simple_image.png'
img.save(filename='simple_image.png')
return simple_image
def get_big_image(self): def get_big_image(self):
with ImageHelper.BigImage as img: with open("./static/big-image.png", "rb") as file:
img = ImageHelper.BigImage return file.read()
big_image = 'big_image.png'
img.save(filename='big_image.png') def save_image(self, img):
return big_image with open("image.png", "wb") as file:
file.write(img)
return True

View File

@@ -1,17 +0,0 @@
from wand.image import Image
class ImageHelper:
SimpleImage = None
BigImage = None
@staticmethod
def load_images():
ImageHelper.SimpleImage = Image(filename="./static/small-image.png")
#ImageHelper.SimpleImage.save(filename="./static/small-image.png")
ImageHelper.BigImage = Image(filename="./static/big-image.png")
#ImageHelper.BigImage.save(filename="./static/big-image.png")
pass
ImageHelper.load_images()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

51
docker-compose.yml Normal file
View File

@@ -0,0 +1,51 @@
version: "3"
services:
tcc-aspnet:
image: tcc:aspnet
container_name: tcc-aspnet
build: ./ASP.NET
restart: always
ports:
- "9081:80"
deploy:
resources:
limits:
cpus: '1'
memory: 1GB
tcc-flask:
image: tcc:flask
container_name: tcc-flask
build: ./FlaskAPI
restart: always
ports:
- "9082:5000"
deploy:
resources:
limits:
cpus: '1'
memory: 1GB
tcc-actix:
image: tcc:actix
container_name: tcc-actix
build: ./ActixAPI
restart: always
ports:
- "9083:9090"
deploy:
resources:
limits:
cpus: '1'
memory: 1GB
tcc-express:
image: tcc:express
container_name: tcc-express
build: ./tcc-express
restart: always
ports:
- "9084:5000"
deploy:
resources:
limits:
cpus: '1'
memory: 1GB

25
scripts/common.py Normal file
View File

@@ -0,0 +1,25 @@
FRAMEWORKS = [
('Actix', 'tcc-actix'),
('ASP.NET', 'tcc-aspnet'),
('Flask', 'tcc-flask'),
('Express', 'tcc-express'),
]
ENDPOINTS = {
'Actix': 'http://localhost:9083',
'ASP.NET': 'http://localhost:9081',
'Flask': 'http://localhost:9082',
'Express': 'http://localhost:9084',
}
BLUR_RADIUS = 5
API_REQUESTS = [
('/status/ok', 'GET', range(0, 30_000, 5000), None),
('/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()),
('/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),
]

126
scripts/graph.py Normal file
View File

@@ -0,0 +1,126 @@
import numpy as np
import matplotlib.pyplot as plt
import os
from common import API_REQUESTS, FRAMEWORKS
FRAMEWORKS = [f for f, _ in FRAMEWORKS]
def plot_graph(x_data, y_data, title, x_label, y_label, filename):
for i, framework in enumerate(FRAMEWORKS):
plt.plot(x_data, y_data[i], markersize=1, linewidth=1, linestyle='solid', label=framework)
plt.title(title)
plt.xlabel(x_label)
plt.ylabel(y_label)
plt.legend()
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
frameworks = {}
print(y_data)
for i, framework in enumerate(FRAMEWORKS):
frameworks[framework] = y_data[i]
x = np.arange(len(requests))
width = 0.10
multiplier = 0
fig, ax = plt.subplots(layout='constrained')
print(x)
for framework, measurements in frameworks.items():
print(framework, measurements)
for attribute, measurement in frameworks.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(frameworks.items()))
ax.set_ylim(0, 120)
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, _ = get_data(filename)
y = []
for f in FRAMEWORKS:
newfile = filename.replace(framework_name, f)
_, y_data = get_data(newfile)
y.append(y_data)
graph_file = f'req_{endpoint_name.replace("/", "").replace("?", "")}'
plot_graph(x, y, f'Requisições atendidas por segundo - {endpoint_name}', 'Número de requisições', 'Requisições/segundo', graph_file)
def generate_resource_graph(filename, framework_name, endpoint_name):
x, _ = get_resource_data(filename)
for resource_index, resource in enumerate(['cpu', 'ram']):
y = []
for f in FRAMEWORKS:
newfile = filename.replace(framework_name, f)
_, y_data = get_resource_data(newfile)
y.append([data[resource_index] for data in y_data])
graph_file = f'{resource}_{endpoint_name.replace("/", "").replace("?", "")}'
plot_resource_graph(x, y, f'Uso de {resource.upper()} - {endpoint_name}', 'Número de requisições', 'Uso (%)', graph_file)
if __name__ == '__main__':
endpoints = [config[0] for config in API_REQUESTS]
for endpoint_name in endpoints:
framework_name = 'ASP.NET'
endpoint_file = endpoint_name.replace('/', '')
filename = f'data/resource_ASP.NET_{endpoint_file}.csv'
generate_resource_graph(filename, framework_name, endpoint_name)
filename = f'data/req_ASP.NET_{endpoint_file}.csv'
generate_req_graph(filename, framework_name, endpoint_name)

17
scripts/init.py Normal file
View File

@@ -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/iFuXSNhw/small-image.png')
download_file('https://files.ivanch.me/api/public/dl/81Bkht5C/big-image.png')

View File

@@ -1,33 +1,140 @@
import requests import requests
import docker
import concurrent.futures import concurrent.futures
import time import time
import sys
import os
from math import floor
from init import init
from common import FRAMEWORKS, ENDPOINTS, API_REQUESTS
URL_BASE = 'http://localhost:5100' init()
num_requests = [1000, 5000, 10_000, 50_000, 100_000, 500_000, 1_000_000] THREADS = 10
FRAMEWORK_NAME = ""
CONTAINER_NAME = ""
URL_BASE = 'http://localhost:9090'
def send_request(session, url): def send_request(url, method = 'GET', payload = None):
response = session.get(url) success = False
return response.status_code 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: for num_request in num_requests:
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: if num_request <= 0: continue
url = f'{URL_BASE}/status/ok'
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() start_time = time.time()
futures = [] futures = []
with requests.Session() as session: #with requests.Session() as session:
futures = [executor.submit(send_request, session, url) for _ in range(num_request)] # 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) concurrent.futures.wait(futures)
elapsed_time = time.time() - start_time 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"{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}]]")
print(f"Successful responses: {successful_responses}/{num_request}") record(files[0], num_request, f"{num_request/elapsed_time:.2f}")
record_resource(files[1], num_request, cpu, ram)
main() 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")
for i in range(len(FRAMEWORKS)):
FRAMEWORK_NAME = FRAMEWORKS[i][0]
CONTAINER_NAME = FRAMEWORKS[i][1]
URL_BASE = ENDPOINTS[FRAMEWORK_NAME]
for endpoint, method, num_requests, metadata in API_REQUESTS:
print(f"# {FRAMEWORK_NAME} - {endpoint}")
run_tests(endpoint, method, num_requests, metadata)