performance improvements + a good amount of changes
This commit is contained in:
parent
89a6d9a528
commit
0649a28a28
52
CHANGELOG.md
Normal file
52
CHANGELOG.md
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
# 0.1.0 Second Pre-Release
|
||||||
|
|
||||||
|
- Allowed multiple clients to be handled at once via multithreadding
|
||||||
|
- Enabled file cacheing in file_backend
|
||||||
|
- Added benchmarking utils
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
- High resource utilization
|
||||||
|
- Content other than HTMl will be treated as `text/html`
|
||||||
|
|
||||||
|
## Benchmark Results
|
||||||
|
|
||||||
|
Each benchmark makes 1000 requests requesting a 21.9Mb file using
|
||||||
|
100 users to the webserver running in a Docker container.
|
||||||
|
|
||||||
|
```
|
||||||
|
=====[ Anthracite Benchmarking Tool ]=====
|
||||||
|
Requests : 1000
|
||||||
|
Users/Threads: 100
|
||||||
|
|
||||||
|
|
||||||
|
====[ anthracite ]=====
|
||||||
|
Average Response Time: 5.9561 seconds
|
||||||
|
p995 Response Time : 65.9016 seconds
|
||||||
|
p99 Response Time : 12.3729 seconds
|
||||||
|
p90 Response Time : 7.8775 seconds
|
||||||
|
p75 Response Time : 5.9906 seconds
|
||||||
|
p50 Response Time : 5.0612 seconds
|
||||||
|
Total Response Time : 5956.0972 seconds
|
||||||
|
====[ nginx ]=====
|
||||||
|
Average Response Time: 6.0074 seconds
|
||||||
|
p995 Response Time : 12.5307 seconds
|
||||||
|
p99 Response Time : 11.9421 seconds
|
||||||
|
p90 Response Time : 8.4831 seconds
|
||||||
|
p75 Response Time : 6.6225 seconds
|
||||||
|
p50 Response Time : 5.5751 seconds
|
||||||
|
Total Response Time : 6007.4155 seconds
|
||||||
|
====[ apache ]=====
|
||||||
|
Average Response Time: 5.9028 seconds
|
||||||
|
p995 Response Time : 10.7204 seconds
|
||||||
|
p99 Response Time : 10.0042 seconds
|
||||||
|
p90 Response Time : 6.9419 seconds
|
||||||
|
p75 Response Time : 6.3974 seconds
|
||||||
|
p50 Response Time : 5.8380 seconds
|
||||||
|
Total Response Time : 5902.8490 seconds
|
||||||
|
==========
|
||||||
|
Total Test Time : 188.6373 seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
# 0.0.0 First Pre-Release
|
||||||
|
- Added file based backend
|
||||||
|
- Support for text/html
|
|
@ -1,7 +1,7 @@
|
||||||
FROM alpine as build-env
|
FROM alpine as build-env
|
||||||
|
|
||||||
RUN apk add --no-cache build-base
|
RUN apk add --no-cache build-base
|
||||||
COPY . .
|
COPY ./src/ .
|
||||||
RUN make build-release
|
RUN make build-release
|
||||||
|
|
||||||
CMD ["/anthracite"]
|
CMD ["/anthracite"]
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
#include "../http.cpp"
|
|
||||||
|
|
||||||
class backend {
|
|
||||||
public:
|
|
||||||
virtual http_response handle_request(http_request req) = 0;
|
|
||||||
};
|
|
2
benchmark/anthracite.Dockerfile
Normal file
2
benchmark/anthracite.Dockerfile
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
FROM anthracite:latest
|
||||||
|
COPY ./www/ /www/
|
3
benchmark/apache.Dockerfile
Executable file
3
benchmark/apache.Dockerfile
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
FROM httpd:2.4
|
||||||
|
WORKDIR /usr/local/apache2/htdocs/
|
||||||
|
COPY ./www/ .
|
69
benchmark/benchmark.py
Normal file
69
benchmark/benchmark.py
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
import math
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from http.client import HTTPConnection
|
||||||
|
|
||||||
|
HTTPConnection._http_vsn_str = 'HTTP/1.0'
|
||||||
|
urls = { 'anthracite': 'http://localhost:8081/large.html','nginx': 'http://localhost:8082/large.html', 'apache': 'http://localhost:8083/large.html' }
|
||||||
|
num_requests = 1000
|
||||||
|
num_users = 100 # number of threads
|
||||||
|
response_times = {}
|
||||||
|
|
||||||
|
def percentile(N, percent, key=lambda x:x):
|
||||||
|
if not N:
|
||||||
|
return None
|
||||||
|
N.sort()
|
||||||
|
k = (len(N)-1) * percent
|
||||||
|
f = math.floor(k)
|
||||||
|
c = math.ceil(k)
|
||||||
|
if f == c:
|
||||||
|
return key(N[int(k)])
|
||||||
|
d0 = key(N[int(f)]) * (c-k)
|
||||||
|
d1 = key(N[int(c)]) * (k-f)
|
||||||
|
return d0+d1
|
||||||
|
|
||||||
|
# Define a function to make an HTTP request and measure the response time
|
||||||
|
def make_request(request_number, server_name):
|
||||||
|
start_time = time.time()
|
||||||
|
response = requests.get(urls[server_name])
|
||||||
|
end_time = time.time()
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
response_time = end_time - start_time
|
||||||
|
response_times[server_name].append(response_time)
|
||||||
|
else:
|
||||||
|
print(f'Request {request_number}: Request failed with status code {response.status_code}')
|
||||||
|
print('=====[ Anthracite Benchmarking Tool ]=====')
|
||||||
|
print(f'Requests : {num_requests}')
|
||||||
|
print(f'Users/Threads: {num_users}\n\n')
|
||||||
|
start_all_time = time.time()
|
||||||
|
|
||||||
|
futures = []
|
||||||
|
|
||||||
|
for server_name in urls:
|
||||||
|
response_times[server_name] = []
|
||||||
|
with ThreadPoolExecutor(max_workers=num_users) as executor:
|
||||||
|
futures += [executor.submit(make_request, i + 1, server_name) for i in range(num_requests)]
|
||||||
|
|
||||||
|
# Wait for all requests to complete
|
||||||
|
for future in futures:
|
||||||
|
future.result()
|
||||||
|
|
||||||
|
end_all_time = time.time()
|
||||||
|
|
||||||
|
for server_name in urls:
|
||||||
|
print(f'====[ {server_name} ]=====')
|
||||||
|
average_response_time = sum(response_times[server_name]) / len(response_times[server_name])
|
||||||
|
total_response_time = sum(response_times[server_name])
|
||||||
|
print(f'Average Response Time: {average_response_time:.4f} seconds')
|
||||||
|
print(f'p995 Response Time : {percentile(response_times[server_name], .995):.4f} seconds')
|
||||||
|
print(f'p99 Response Time : {percentile(response_times[server_name], .99):.4f} seconds')
|
||||||
|
print(f'p90 Response Time : {percentile(response_times[server_name], .90):.4f} seconds')
|
||||||
|
print(f'p75 Response Time : {percentile(response_times[server_name], .75):.4f} seconds')
|
||||||
|
print(f'p50 Response Time : {percentile(response_times[server_name], .50):.4f} seconds')
|
||||||
|
print(f'Total Response Time : {total_response_time:.4f} seconds')
|
||||||
|
|
||||||
|
total_test_time = end_all_time - start_all_time
|
||||||
|
print('==========')
|
||||||
|
print(f'Total Test Time : {total_test_time:.4f} seconds')
|
19
benchmark/docker-compose.yaml
Normal file
19
benchmark/docker-compose.yaml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
services:
|
||||||
|
anthracite:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: anthracite.Dockerfile
|
||||||
|
ports:
|
||||||
|
- "8081:80"
|
||||||
|
nginx:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: nginx.Dockerfile
|
||||||
|
ports:
|
||||||
|
- "8082:80"
|
||||||
|
apache:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: apache.Dockerfile
|
||||||
|
ports:
|
||||||
|
- "8083:80"
|
2
benchmark/nginx.Dockerfile
Normal file
2
benchmark/nginx.Dockerfile
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
FROM nginx:alpine
|
||||||
|
COPY ./www/ /usr/share/nginx/html
|
5
benchmark/rebuild-container.sh
Executable file
5
benchmark/rebuild-container.sh
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
cd ..
|
||||||
|
docker build . -t anthracite:latest
|
||||||
|
cd benchmark
|
||||||
|
docker build . -t benchmark-anthracite -f anthracite.Dockerfile
|
||||||
|
docker compose up -d
|
1020251
benchmark/www/large.html
Normal file
1020251
benchmark/www/large.html
Normal file
File diff suppressed because it is too large
Load diff
52
main.cpp
52
main.cpp
|
@ -1,52 +0,0 @@
|
||||||
#include "backends/file_backend.cpp"
|
|
||||||
#include <exception>
|
|
||||||
#include <fstream>
|
|
||||||
#include <iostream>
|
|
||||||
#include <netinet/in.h>
|
|
||||||
#include <sstream>
|
|
||||||
#include <sys/socket.h>
|
|
||||||
#include <thread>
|
|
||||||
#include <unistd.h>
|
|
||||||
#include <unordered_map>
|
|
||||||
|
|
||||||
using namespace std;
|
|
||||||
|
|
||||||
void log_request_an_response(http_request req, http_response resp);
|
|
||||||
|
|
||||||
constexpr int default_port = 80;
|
|
||||||
|
|
||||||
void handle_client(anthracite_socket s, file_backend fb)
|
|
||||||
{
|
|
||||||
http_request req(s);
|
|
||||||
http_response resp = fb.handle_request(req);
|
|
||||||
log_request_an_response(req, resp);
|
|
||||||
s.send_message(resp.to_string());
|
|
||||||
s.close_conn();
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char** argv)
|
|
||||||
{
|
|
||||||
int port_number = default_port;
|
|
||||||
|
|
||||||
if (argc > 1) {
|
|
||||||
port_number = atoi(argv[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
cout << "Initializing Anthracite" << endl;
|
|
||||||
anthracite_socket s(port_number);
|
|
||||||
file_backend fb(false);
|
|
||||||
cout << "Initialization Complete" << endl;
|
|
||||||
cout << "Listening for HTTP connections on port " << port_number << endl;
|
|
||||||
|
|
||||||
while(true) {
|
|
||||||
s.wait_for_conn();
|
|
||||||
thread(handle_client, s, ref(fb)).detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void log_request_an_response(http_request req, http_response resp)
|
|
||||||
{
|
|
||||||
cout << "[" << resp.status_code() << " " + http_status_map.find(resp.status_code())->second + "] " + req.client_ip() + " " + http_reverse_method_map.find(req.method())->second + " " + req.path() << endl;
|
|
||||||
}
|
|
|
@ -1,10 +1,10 @@
|
||||||
.PHONY: format lint build build-release build-docker run debug
|
.PHONY: format lint build build-release build-docker run debug
|
||||||
|
|
||||||
build:
|
build:
|
||||||
g++ main.cpp -g -o anthracite
|
g++ main.cpp -g -o ./anthracite
|
||||||
|
|
||||||
build-release:
|
build-release:
|
||||||
g++ main.cpp -O2 -o anthracite
|
g++ main.cpp -O3 -march=native -o ./anthracite
|
||||||
|
|
||||||
build-docker:
|
build-docker:
|
||||||
docker build . -t anthracite
|
docker build . -t anthracite
|
7
src/backends/backend.cpp
Normal file
7
src/backends/backend.cpp
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
#include "../http.cpp"
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
class backend {
|
||||||
|
public:
|
||||||
|
virtual unique_ptr<http_response> handle_request(http_request& req) = 0;
|
||||||
|
};
|
|
@ -6,7 +6,7 @@ private:
|
||||||
unordered_map<string, string> file_cache;
|
unordered_map<string, string> file_cache;
|
||||||
bool cache_enabled;
|
bool cache_enabled;
|
||||||
|
|
||||||
http_response handle_request_nocache(http_request req) {
|
unique_ptr<http_response> handle_request_nocache(http_request& req) {
|
||||||
string filename = req.path() == "/" ? "index.html" : req.path();
|
string filename = req.path() == "/" ? "index.html" : req.path();
|
||||||
filename = "./www/" + filename;
|
filename = "./www/" + filename;
|
||||||
ifstream stream(filename);
|
ifstream stream(filename);
|
||||||
|
@ -20,10 +20,10 @@ private:
|
||||||
|
|
||||||
stringstream buffer;
|
stringstream buffer;
|
||||||
buffer << stream.rdbuf();
|
buffer << stream.rdbuf();
|
||||||
return { buffer.str(), status };
|
return make_unique<http_response>(buffer.str(), status);
|
||||||
}
|
}
|
||||||
|
|
||||||
http_response handle_request_cache(http_request req) {
|
unique_ptr<http_response> handle_request_cache(http_request& req) {
|
||||||
string filename = req.path() == "/" ? "/index.html" : req.path();
|
string filename = req.path() == "/" ? "/index.html" : req.path();
|
||||||
filename = "./www" + filename;
|
filename = "./www" + filename;
|
||||||
auto file_info = file_cache.find(filename);
|
auto file_info = file_cache.find(filename);
|
||||||
|
@ -35,7 +35,7 @@ private:
|
||||||
file_info = file_cache.find(filename);
|
file_info = file_cache.find(filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { file_info->second, status };
|
return make_unique<http_response>(file_info->second, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
void populate_cache_dir(string dir) {
|
void populate_cache_dir(string dir) {
|
||||||
|
@ -66,7 +66,7 @@ public:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
http_response handle_request(http_request req) override {
|
unique_ptr<http_response> handle_request(http_request& req) override {
|
||||||
return cache_enabled ? handle_request_cache(req) : handle_request_nocache(req);
|
return cache_enabled ? handle_request_cache(req) : handle_request_nocache(req);
|
||||||
}
|
}
|
||||||
};
|
};
|
|
@ -4,6 +4,7 @@
|
||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "socket.cpp"
|
||||||
#include <exception>
|
#include <exception>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
@ -12,8 +13,6 @@
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "socket.cpp"
|
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
||||||
constexpr int HTTP_HEADER_BYTES = 8190;
|
constexpr int HTTP_HEADER_BYTES = 8190;
|
||||||
|
@ -235,7 +234,7 @@ private:
|
||||||
unordered_map<string, query_param> _query_params; // kinda goofy, whatever
|
unordered_map<string, query_param> _query_params; // kinda goofy, whatever
|
||||||
|
|
||||||
public:
|
public:
|
||||||
http_request(anthracite_socket s)
|
http_request(anthracite_socket& s)
|
||||||
: _path("")
|
: _path("")
|
||||||
{
|
{
|
||||||
string raw_data = s.recv_message(HTTP_HEADER_BYTES);
|
string raw_data = s.recv_message(HTTP_HEADER_BYTES);
|
69
src/main.cpp
Normal file
69
src/main.cpp
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
#include "backends/file_backend.cpp"
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <exception>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <mutex>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <sstream>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <thread>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
void log_request_and_response(http_request& req, unique_ptr<http_response>& resp);
|
||||||
|
|
||||||
|
constexpr int default_port = 80;
|
||||||
|
|
||||||
|
int active_threads = 0;
|
||||||
|
mutex mtx;
|
||||||
|
condition_variable cv;
|
||||||
|
|
||||||
|
void handle_client(anthracite_socket s, file_backend& fb)
|
||||||
|
{
|
||||||
|
http_request req(s);
|
||||||
|
unique_ptr<http_response> resp = fb.handle_request(req);
|
||||||
|
log_request_and_response(req, resp);
|
||||||
|
s.send_message(resp->to_string());
|
||||||
|
resp.reset();
|
||||||
|
s.close_conn();
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mtx);
|
||||||
|
active_threads--;
|
||||||
|
}
|
||||||
|
|
||||||
|
cv.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
int port_number = default_port;
|
||||||
|
|
||||||
|
if (argc > 1) {
|
||||||
|
port_number = atoi(argv[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
cout << "Initializing Anthracite" << endl;
|
||||||
|
anthracite_socket s(port_number);
|
||||||
|
file_backend fb(true);
|
||||||
|
cout << "Initialization Complete" << endl;
|
||||||
|
cout << "Listening for HTTP connections on port " << port_number << endl;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
s.wait_for_conn();
|
||||||
|
std::unique_lock<std::mutex> lock(mtx);
|
||||||
|
cv.wait(lock, [] { return active_threads < 20; });
|
||||||
|
active_threads++;
|
||||||
|
thread(handle_client, s, ref(fb)).detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void log_request_and_response(http_request& req, unique_ptr<http_response>& resp)
|
||||||
|
{
|
||||||
|
cout << "[" << resp->status_code() << " " + http_status_map.find(resp->status_code())->second + "] " + req.client_ip() + " " + http_reverse_method_map.find(req.method())->second + " " + req.path() << endl;
|
||||||
|
}
|
|
@ -20,7 +20,7 @@ private:
|
||||||
socklen_t client_addr_len {};
|
socklen_t client_addr_len {};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
anthracite_socket(int port, int max_queue = 100)
|
anthracite_socket(int port, int max_queue = 10)
|
||||||
: server_socket(socket(AF_INET, SOCK_STREAM, 0))
|
: server_socket(socket(AF_INET, SOCK_STREAM, 0))
|
||||||
, client_ip("")
|
, client_ip("")
|
||||||
{
|
{
|
1020251
src/www/large.html
Normal file
1020251
src/www/large.html
Normal file
File diff suppressed because it is too large
Load diff
3
src/www/test.html
Normal file
3
src/www/test.html
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<center>
|
||||||
|
<h1>Test Page!</h1>
|
||||||
|
</center>
|
Loading…
Reference in a new issue