init commit
This commit is contained in:
commit
8fe9084295
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
.vs
|
||||
.idea
|
||||
__pycache__
|
||||
9
README.md
Normal file
9
README.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Performance Comparison of Communication Protocols in Microservices
|
||||
|
||||
**HTTP RESTful** vs **gRPC**
|
||||
|
||||

|
||||
|
||||
# Architecture
|
||||
|
||||

|
||||
14
rest_grpc_compare_code/Dockerfile
Normal file
14
rest_grpc_compare_code/Dockerfile
Normal file
@ -0,0 +1,14 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
RUN chmod +x run_server.sh
|
||||
|
||||
EXPOSE 9520
|
||||
|
||||
# 启动脚本
|
||||
CMD ["./run_server.sh"]
|
||||
86
rest_grpc_compare_code/grpc_client.py
Normal file
86
rest_grpc_compare_code/grpc_client.py
Normal file
@ -0,0 +1,86 @@
|
||||
import grpc
|
||||
from server.grpc.proto import Book_pb2, Book_pb2_grpc
|
||||
from utils import get_size_from_dict, NetworkingSize
|
||||
|
||||
|
||||
class TrafficInterceptor(grpc.UnaryUnaryClientInterceptor):
|
||||
def __init__(self):
|
||||
self.last_call_info = None
|
||||
|
||||
def intercept_unary_unary(self, continuation, client_call_details, request):
|
||||
request_size = len(request.SerializeToString())
|
||||
response = continuation(client_call_details, request)
|
||||
response_size = len(response.result().SerializeToString())
|
||||
|
||||
self.last_call_info = {
|
||||
'request_bytes': request_size,
|
||||
'response_bytes': response_size
|
||||
}
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class GrpcClient:
|
||||
def __init__(self, address='localhost:50051'):
|
||||
self.traffic_interceptor = TrafficInterceptor()
|
||||
|
||||
base_channel = grpc.insecure_channel(address, options=[
|
||||
('grpc.max_receive_message_length', -1),
|
||||
('grpc.max_send_message_length', -1),
|
||||
])
|
||||
self.channel = grpc.intercept_channel(
|
||||
base_channel,
|
||||
self.traffic_interceptor
|
||||
)
|
||||
|
||||
self.stub = Book_pb2_grpc.BookServiceStub(self.channel)
|
||||
|
||||
def get_list(self, pages=1, per_page=10000, list_data_limit=30):
|
||||
try:
|
||||
request = Book_pb2.GetListRequest(pages=pages, per_page=per_page, list_data_limit=list_data_limit)
|
||||
response = self.stub.GetList(request)
|
||||
return response, get_size_from_dict(self.traffic_interceptor.last_call_info)
|
||||
except grpc.RpcError as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
def add_books(self, books, test_only=False):
|
||||
try:
|
||||
request = Book_pb2.AddBookRequest(books=books, test_only=test_only)
|
||||
response = self.stub.AddBooks(request)
|
||||
return response, get_size_from_dict(self.traffic_interceptor.last_call_info)
|
||||
except grpc.RpcError as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
def delete_books(self, book_ids, delete_last_count=-1):
|
||||
try:
|
||||
request = Book_pb2.DeleteBookRequest(book_ids=book_ids, delete_last_count=delete_last_count)
|
||||
response = self.stub.DeleteBooks(request)
|
||||
return response, get_size_from_dict(self.traffic_interceptor.last_call_info)
|
||||
except grpc.RpcError as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
def update_book(self, book):
|
||||
try:
|
||||
request = Book_pb2.UpdateBookRequest(book=book)
|
||||
response = self.stub.UpdateBook(request)
|
||||
return response, get_size_from_dict(self.traffic_interceptor.last_call_info)
|
||||
except grpc.RpcError as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
def ping(self):
|
||||
try:
|
||||
self.stub.Ping(Book_pb2.Empty())
|
||||
except grpc.RpcError as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
def close(self):
|
||||
# 程序结束时关闭连接
|
||||
self.channel.close()
|
||||
|
||||
|
||||
def run_client():
|
||||
GrpcClient().get_list(1, 10000)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_client()
|
||||
67
rest_grpc_compare_code/main.py
Normal file
67
rest_grpc_compare_code/main.py
Normal file
@ -0,0 +1,67 @@
|
||||
from flask import Flask
|
||||
from flask import request, jsonify
|
||||
from flask_cors import CORS
|
||||
import performance_test
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app, supports_credentials=True)
|
||||
REQUEST_PASSWORD = "test123456"
|
||||
|
||||
|
||||
def return_json(data=None, message="", success=True, status_code=200):
|
||||
return jsonify({
|
||||
"success": success,
|
||||
"message": message,
|
||||
"data": data
|
||||
}), status_code
|
||||
|
||||
|
||||
def error_catch_and_password_check(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
if request.headers.get('password') != REQUEST_PASSWORD:
|
||||
return return_json(message="Unauthorized", success=False, status_code=403)
|
||||
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
# raise e
|
||||
return return_json(message=f"Error: {repr(e)}", success=False, status_code=500)
|
||||
|
||||
wrapper.__name__ = func.__name__
|
||||
return wrapper
|
||||
|
||||
@app.route('/ping', methods=['GET'])
|
||||
@error_catch_and_password_check
|
||||
def ping_request():
|
||||
return return_json(message="pong", success=True, status_code=200)
|
||||
|
||||
@app.route("/performance_test_get_list", methods=["GET"])
|
||||
@error_catch_and_password_check
|
||||
def performance_test_get_list_request():
|
||||
per_page = request.args.get("count", default=10000, type=int)
|
||||
example_data_limit = request.args.get("example_data_limit", default=10, type=int)
|
||||
idx = request.args.get("idx", default=-1, type=int)
|
||||
grpc_result, rest_result = performance_test.test_get_main(per_page=per_page, list_data_limit=example_data_limit)
|
||||
return return_json({
|
||||
"idx": idx,
|
||||
"request_count": per_page,
|
||||
"grpc": grpc_result,
|
||||
"rest": rest_result
|
||||
})
|
||||
|
||||
@app.route("/performance_test_add_books", methods=["GET"])
|
||||
@error_catch_and_password_check
|
||||
def performance_test_add_books_request():
|
||||
book_count = request.args.get("count", default=1000, type=int)
|
||||
grpc_result, rest_result = performance_test.test_add_main(book_count=book_count)
|
||||
idx = request.args.get("idx", default=-1, type=int)
|
||||
return return_json({
|
||||
"idx": idx,
|
||||
"request_count": book_count,
|
||||
"grpc": grpc_result,
|
||||
"rest": rest_result
|
||||
})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run("0.0.0.0", port=9520)
|
||||
163
rest_grpc_compare_code/performance_test.py
Normal file
163
rest_grpc_compare_code/performance_test.py
Normal file
@ -0,0 +1,163 @@
|
||||
import json
|
||||
import time
|
||||
from dataclasses import dataclass, asdict
|
||||
from typing import Any
|
||||
from grpc_client import GrpcClient
|
||||
from rest_client import RestClient
|
||||
from utils import NetworkingSize, generate_random_book_data
|
||||
from google.protobuf.json_format import MessageToDict
|
||||
from google._upb._message import RepeatedCompositeContainer
|
||||
import psutil
|
||||
import copy
|
||||
|
||||
# SERVER_ADDRESS = "localhost"
|
||||
# REST_SERVER_PORT = 9500
|
||||
# GRPC_SERVER_PORT = 50051
|
||||
|
||||
@dataclass
|
||||
class PerformanceResult:
|
||||
client_networking_ping: float = 0.0
|
||||
client_request_time: float = 0.0
|
||||
client_request_cpu: float = 0.0
|
||||
server_deserialize_time: float = 0.0
|
||||
server_deserialize_cpu: float = 0.0
|
||||
server_serialize_time: float = 0.0
|
||||
server_serialize_cpu: float = 0.0
|
||||
server_protocol_total_time: float = 0.0
|
||||
response_data: Any = None
|
||||
networking_size: NetworkingSize = None
|
||||
|
||||
|
||||
class ClientPerformanceTest:
|
||||
def __init__(self, server_address, rest_server_port, grpc_server_port):
|
||||
self.grpc = GrpcClient(f"{server_address}:{grpc_server_port}")
|
||||
self.rest = RestClient(f"http://{server_address}:{rest_server_port}")
|
||||
|
||||
@staticmethod
|
||||
def ping_test(func, count=5):
|
||||
time_total = 0
|
||||
for i in range(count):
|
||||
time_start = time.perf_counter()
|
||||
func()
|
||||
time_end = time.perf_counter()
|
||||
time_total += (time_end - time_start)
|
||||
time.sleep(0.1)
|
||||
return time_total / count
|
||||
|
||||
@staticmethod
|
||||
def client_performance_test(networking_ping, func, *args, **kwargs):
|
||||
# networking_ping = ping_test(grpc.ping, count=5)
|
||||
process = psutil.Process()
|
||||
cpu_count = psutil.cpu_count()
|
||||
|
||||
request_start_time = time.perf_counter()
|
||||
process.cpu_percent()
|
||||
# cpu_request_start = process.cpu_times()
|
||||
|
||||
# result = grpc.get_list(pages=page, per_page=per_page)
|
||||
result, networking_size = func(*args, **kwargs)
|
||||
|
||||
cpu_request_percent = process.cpu_percent()
|
||||
# cpu_request_end = process.cpu_times()
|
||||
request_end_time = time.perf_counter()
|
||||
client_request_time = request_end_time - request_start_time
|
||||
|
||||
# CPU time seconds
|
||||
# client_request_cpu_time = (cpu_request_end.user - cpu_request_start.user) + \
|
||||
# (cpu_request_end.system - cpu_request_start.system)
|
||||
|
||||
# client_request_cpu_usage = client_request_cpu_time / client_request_time * 100
|
||||
|
||||
server_deserialize_time = result.server_deserialize.time
|
||||
server_deserialize_cpu = result.server_deserialize.cpu
|
||||
|
||||
server_serialize_time = result.server_serialize.time
|
||||
server_serialize_cpu = result.server_serialize.cpu
|
||||
|
||||
server_protocol_total_time = result.server_protocol_total_time
|
||||
|
||||
return PerformanceResult(client_networking_ping=networking_ping,
|
||||
client_request_time=client_request_time,
|
||||
client_request_cpu=cpu_request_percent / cpu_count,
|
||||
server_deserialize_time=server_deserialize_time,
|
||||
server_deserialize_cpu=server_deserialize_cpu,
|
||||
server_serialize_time=server_serialize_time,
|
||||
server_serialize_cpu=server_serialize_cpu,
|
||||
server_protocol_total_time=server_protocol_total_time,
|
||||
response_data=result.response_data,
|
||||
networking_size=networking_size)
|
||||
|
||||
def test_get_list_performance_grpc(self, per_page=10000, page=1, list_data_limit=30):
|
||||
networking_ping = self.ping_test(self.grpc.ping, count=3)
|
||||
return self.client_performance_test(networking_ping, self.grpc.get_list, pages=page, per_page=per_page
|
||||
, list_data_limit=list_data_limit)
|
||||
|
||||
def test_get_list_performance_rest(self, per_page=10000, page=1, list_data_limit=30):
|
||||
networking_ping = self.ping_test(self.rest.ping, count=3)
|
||||
return self.client_performance_test(networking_ping, self.rest.get_list, pages=page, per_page=per_page,
|
||||
list_data_limit=list_data_limit)
|
||||
|
||||
def _test_add_books_performance_grpc(self, books, test_only=False):
|
||||
networking_ping = self.ping_test(self.grpc.ping, count=3)
|
||||
return self.client_performance_test(networking_ping, self.grpc.add_books, books=books, test_only=test_only)
|
||||
|
||||
def _test_add_books_performance_rest(self, books, test_only=False):
|
||||
networking_ping = self.ping_test(self.rest.ping, count=3,)
|
||||
return self.client_performance_test(networking_ping, self.rest.add_book, books=books, test_only=test_only)
|
||||
|
||||
def test_add_books_performance(self, book_count=1000, test_only=True):
|
||||
books_data = generate_random_book_data(book_count)
|
||||
grpc_result = self._test_add_books_performance_grpc(books_data, test_only=test_only)
|
||||
if not test_only:
|
||||
self.grpc.delete_books([], book_count)
|
||||
rest_result = self._test_add_books_performance_rest(books_data, test_only=test_only)
|
||||
if not test_only:
|
||||
self.rest.delete_books([], book_count)
|
||||
return grpc_result, rest_result
|
||||
|
||||
|
||||
def test_get_main(per_page=1_0000, list_data_limit=100):
|
||||
client_performance_test = ClientPerformanceTest("127.0.0.1", 9500, 50051)
|
||||
grpc_result = client_performance_test.test_get_list_performance_grpc(per_page=per_page, page=1)
|
||||
if len(grpc_result.response_data) > list_data_limit:
|
||||
grpc_result.response_data = grpc_result.response_data[:list_data_limit]
|
||||
|
||||
if isinstance(grpc_result.response_data, RepeatedCompositeContainer):
|
||||
grpc_result.response_data = list(grpc_result.response_data)
|
||||
|
||||
for n, i in enumerate(grpc_result.response_data):
|
||||
grpc_result.response_data[n] = MessageToDict(i, preserving_proto_field_name=True)
|
||||
|
||||
if len(grpc_result.response_data) <= 0:
|
||||
grpc_result.response_data = []
|
||||
|
||||
# result_dict = copy.deepcopy(grpc_result.__dict__)
|
||||
# result_dict.pop("response_data")
|
||||
# print("gRPC Performance Result:", result_dict)
|
||||
|
||||
rest_result = client_performance_test.test_get_list_performance_rest(per_page=per_page, page=1)
|
||||
if len(rest_result.response_data) > list_data_limit:
|
||||
rest_result.response_data = rest_result.response_data[:list_data_limit]
|
||||
|
||||
for n, i in enumerate(rest_result.response_data):
|
||||
rest_result.response_data[n] = dict(i)
|
||||
|
||||
# result_dict = copy.deepcopy(rest_result.__dict__)
|
||||
# result_dict.pop("response_data")
|
||||
# print("REST Performance Result:", result_dict)
|
||||
return asdict(grpc_result), asdict(rest_result)
|
||||
|
||||
|
||||
def test_add_main(book_count=1_0000):
|
||||
client_performance_test = ClientPerformanceTest("127.0.0.1", 9500, 50051)
|
||||
grpc_result, rest_result = client_performance_test.test_add_books_performance(book_count=book_count)
|
||||
grpc_result.response_data = MessageToDict(grpc_result.response_data, preserving_proto_field_name=True)
|
||||
rest_result.response_data = dict(rest_result.response_data)
|
||||
# print("gRPC Add Books Performance Result:", grpc_result)
|
||||
# print("REST Add Books Performance Result:", rest_result)
|
||||
return asdict(grpc_result), asdict(rest_result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(test_get_main(10000, 2))
|
||||
# test_add_main()
|
||||
13
rest_grpc_compare_code/requirements.txt
Normal file
13
rest_grpc_compare_code/requirements.txt
Normal file
@ -0,0 +1,13 @@
|
||||
protobuf~=6.31.1
|
||||
psycopg2-binary~=2.9.10
|
||||
grpcio~=1.74.0
|
||||
grpcio-tools~=1.74.0
|
||||
SQLAlchemy~=2.0.42
|
||||
tqdm~=4.67.1
|
||||
Faker~=37.5.3
|
||||
psutil~=7.0.0
|
||||
Flask~=3.1.1
|
||||
flask-cors~=6.0.1
|
||||
flask-sqlalchemy~=3.0.2
|
||||
requests~=2.32.4
|
||||
pydantic~=2.11.7
|
||||
101
rest_grpc_compare_code/rest_client.py
Normal file
101
rest_grpc_compare_code/rest_client.py
Normal file
@ -0,0 +1,101 @@
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from typing import List, Dict, Any, Tuple
|
||||
from rest_client_models import GetListResponse, GeneralResponse, MessageResponse
|
||||
from utils import get_size_from_dict, NetworkingSize
|
||||
|
||||
|
||||
class TrafficAdapter(HTTPAdapter):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.last_request_size = 0
|
||||
self.last_response_size = 0
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def send(self, request, **kwargs):
|
||||
self.last_request_size = len(request.method.encode()) + len(request.url.encode()) + 12 # "HTTP/1.1\r\n"
|
||||
|
||||
if request.headers:
|
||||
for name, value in request.headers.items():
|
||||
self.last_request_size += len(name.encode()) + len(value.encode()) + 4 # ": " + "\r\n"
|
||||
self.last_request_size += 2 # "\r\n"
|
||||
|
||||
if request.body:
|
||||
if isinstance(request.body, bytes):
|
||||
self.last_request_size += len(request.body)
|
||||
else:
|
||||
self.last_request_size += len(str(request.body).encode())
|
||||
|
||||
response = super().send(request, **kwargs)
|
||||
|
||||
self.last_response_size = len(f"HTTP/1.1 {response.status_code}".encode()) + 2
|
||||
|
||||
if response.headers:
|
||||
for name, value in response.headers.items():
|
||||
self.last_response_size += len(name.encode()) + len(value.encode()) + 4
|
||||
self.last_response_size += 2 # "\r\n"
|
||||
self.last_response_size += len(response.content)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class TrafficSession(requests.Session):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.traffic_adapter = TrafficAdapter()
|
||||
self.mount('http://', self.traffic_adapter)
|
||||
self.mount('https://', self.traffic_adapter)
|
||||
|
||||
def get_last_traffic(self):
|
||||
return {
|
||||
'request_bytes': self.traffic_adapter.last_request_size,
|
||||
'response_bytes': self.traffic_adapter.last_response_size
|
||||
}
|
||||
|
||||
class RestClient:
|
||||
def __init__(self, base_url):
|
||||
self.base_url = base_url
|
||||
# self.session = requests.Session()
|
||||
self.session = TrafficSession()
|
||||
|
||||
def get_list(self, pages: int = 1, per_page: int = 100, list_data_limit=30) -> Tuple[GetListResponse, NetworkingSize]:
|
||||
response = self.session.get(
|
||||
f"{self.base_url}/api/get_list",
|
||||
params={"pages": pages, "per_page": per_page, "list_data_limit": list_data_limit}
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
return GetListResponse(**data), get_size_from_dict(self.session.get_last_traffic())
|
||||
|
||||
def add_book(self, books: List[Dict[str, Any]], test_only=False) -> Tuple[GeneralResponse, NetworkingSize]:
|
||||
data = {"books": books, "test_only": test_only}
|
||||
response = self.session.post(
|
||||
f"{self.base_url}/api/add_book",
|
||||
json=data
|
||||
)
|
||||
response.raise_for_status()
|
||||
return GeneralResponse(**response.json()), get_size_from_dict(self.session.get_last_traffic())
|
||||
|
||||
def delete_books(self, book_ids: List[int], delete_last_count=-1) -> Tuple[GeneralResponse, NetworkingSize]:
|
||||
data = {"book_ids": book_ids, "delete_last_count": delete_last_count}
|
||||
response = self.session.delete(
|
||||
f"{self.base_url}/api/delete_books",
|
||||
json=data
|
||||
)
|
||||
response.raise_for_status()
|
||||
return GeneralResponse(**response.json()), get_size_from_dict(self.session.get_last_traffic())
|
||||
|
||||
def update_book(self, book_data: Dict[str, Any]) -> Tuple[GeneralResponse, NetworkingSize]:
|
||||
data = {"book": book_data}
|
||||
response = self.session.put(
|
||||
f"{self.base_url}/api/update_book",
|
||||
json=data
|
||||
)
|
||||
response.raise_for_status()
|
||||
return GeneralResponse(**response.json()), get_size_from_dict(self.session.get_last_traffic())
|
||||
|
||||
def ping(self):
|
||||
self.session.get(f"{self.base_url}/api/ping")
|
||||
|
||||
def close(self):
|
||||
self.session.close()
|
||||
54
rest_grpc_compare_code/rest_client_models.py
Normal file
54
rest_grpc_compare_code/rest_client_models.py
Normal file
@ -0,0 +1,54 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class BookInfo(BaseModel):
|
||||
abstract: Optional[str] = ""
|
||||
author: Optional[str] = ""
|
||||
barcode: Optional[str] = ""
|
||||
binding: Optional[str] = ""
|
||||
category_id: Optional[int] = 0
|
||||
cover_image: Optional[str] = ""
|
||||
description: Optional[str] = ""
|
||||
edition: Optional[str] = ""
|
||||
editor: Optional[str] = ""
|
||||
format: Optional[str] = ""
|
||||
id: Optional[int] = 0
|
||||
isbn: Optional[str] = ""
|
||||
keywords: Optional[str] = ""
|
||||
language: Optional[str] = ""
|
||||
pages: Optional[int] = 0
|
||||
publication_date: Optional[int] = 0 # Unix timestamp
|
||||
publisher: Optional[str] = ""
|
||||
subject: Optional[str] = ""
|
||||
subtitle: Optional[str] = ""
|
||||
title: Optional[str] = ""
|
||||
translator: Optional[str] = ""
|
||||
weight: Optional[float] = 0.0
|
||||
|
||||
class Config:
|
||||
extra = "ignore"
|
||||
exclude_none = True
|
||||
|
||||
|
||||
class ServerDeserialize(BaseModel):
|
||||
cpu: float = 0.0
|
||||
time: float = 0.0
|
||||
|
||||
|
||||
class MessageResponse(BaseModel):
|
||||
message: str = ""
|
||||
|
||||
|
||||
class GeneralResponse(BaseModel):
|
||||
response_data: MessageResponse = Field(default_factory=MessageResponse)
|
||||
server_deserialize: ServerDeserialize = Field(default_factory=ServerDeserialize)
|
||||
server_protocol_total_time: float = 0.0
|
||||
server_serialize: ServerDeserialize = Field(default_factory=ServerDeserialize, alias="server_serialize")
|
||||
|
||||
|
||||
class GetListResponse(BaseModel):
|
||||
response_data: List[BookInfo] = Field(default_factory=list)
|
||||
server_deserialize: ServerDeserialize = Field(default_factory=ServerDeserialize)
|
||||
server_protocol_total_time: float = 0.0
|
||||
server_serialize: ServerDeserialize = Field(default_factory=ServerDeserialize)
|
||||
BIN
rest_grpc_compare_code/rest_grpc_compare_code.zip
Normal file
BIN
rest_grpc_compare_code/rest_grpc_compare_code.zip
Normal file
Binary file not shown.
2
rest_grpc_compare_code/run_server.bat
Normal file
2
rest_grpc_compare_code/run_server.bat
Normal file
@ -0,0 +1,2 @@
|
||||
cd server/grpc && start py grpc_server.py -p 50051
|
||||
cd ../rest && start py server_rest.py -p 9500
|
||||
7
rest_grpc_compare_code/run_server.sh
Normal file
7
rest_grpc_compare_code/run_server.sh
Normal file
@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
python main.py &
|
||||
cd server/grpc && python grpc_server.py -p 50051 &
|
||||
cd server/rest && python server_rest.py -p 9500 &
|
||||
|
||||
wait
|
||||
221
rest_grpc_compare_code/server/grpc/database.py
Normal file
221
rest_grpc_compare_code/server/grpc/database.py
Normal file
@ -0,0 +1,221 @@
|
||||
from sqlalchemy import create_engine, select, func, Column, Integer, String, Text, Float, BigInteger
|
||||
from sqlalchemy.orm import sessionmaker, Session, declarative_base
|
||||
from sqlalchemy.pool import QueuePool
|
||||
import sqlalchemy
|
||||
from datetime import datetime
|
||||
from tqdm import tqdm
|
||||
from enum import Enum
|
||||
from faker import Faker
|
||||
import random
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from typing import List, Optional, Any
|
||||
from dataclasses import dataclass
|
||||
|
||||
engine = create_engine(
|
||||
# 'sqlite:///rest_server_books.db',
|
||||
'postgresql://postgres:liuyanfeng66@localhost:5432/BookRestGrpcCompare',
|
||||
poolclass=QueuePool,
|
||||
pool_size=30,
|
||||
max_overflow=50,
|
||||
pool_pre_ping=True,
|
||||
echo=False,
|
||||
# connect_args={
|
||||
# "check_same_thread": False,
|
||||
# "timeout": 30 # 文件锁最长等待秒数,视情况可调
|
||||
# }
|
||||
)
|
||||
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
@dataclass
|
||||
class PaginationResult:
|
||||
"""分页结果数据类"""
|
||||
items: List[Any]
|
||||
total: int
|
||||
page: int
|
||||
per_page: int
|
||||
pages: int
|
||||
has_prev: bool
|
||||
has_next: bool
|
||||
prev_num: Optional[int]
|
||||
next_num: Optional[int]
|
||||
|
||||
|
||||
class Paginator:
|
||||
def __init__(self, session: Session):
|
||||
self.session = session
|
||||
|
||||
def paginate(self, query, page: int = 1, per_page: int = 20, error_out: bool = True) -> PaginationResult:
|
||||
if per_page <= 0:
|
||||
return PaginationResult(
|
||||
items=[],
|
||||
total=0,
|
||||
page=1,
|
||||
per_page=0,
|
||||
pages=0,
|
||||
has_prev=False,
|
||||
has_next=False,
|
||||
prev_num=None,
|
||||
next_num=None
|
||||
)
|
||||
|
||||
page = max(1, page)
|
||||
per_page = max(1, per_page)
|
||||
|
||||
# Get total count of items
|
||||
count_query = select(func.count()).select_from(query.statement.alias())
|
||||
total = self.session.scalar(count_query)
|
||||
|
||||
# Get total pages
|
||||
pages = (total + per_page - 1) // per_page
|
||||
|
||||
if error_out and page > pages and total > 0:
|
||||
raise ValueError(f"Page {page} out of range (1-{pages})")
|
||||
|
||||
offset = (page - 1) * per_page
|
||||
|
||||
items = query.offset(offset).limit(per_page).all()
|
||||
|
||||
return PaginationResult(
|
||||
items=items,
|
||||
total=total,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
pages=pages,
|
||||
has_prev=page > 1,
|
||||
has_next=page < pages,
|
||||
prev_num=page - 1 if page > 1 else None,
|
||||
next_num=page + 1 if page < pages else None
|
||||
)
|
||||
|
||||
|
||||
class BaseModel(Base):
|
||||
__abstract__ = True
|
||||
|
||||
def to_dict(self, exclude: set = None):
|
||||
exclude = exclude or set()
|
||||
ret = {}
|
||||
for c in self.__table__.columns:
|
||||
if c.name not in exclude:
|
||||
curr_item = getattr(self, c.name)
|
||||
if isinstance(curr_item, Enum):
|
||||
ret[c.name] = curr_item.value
|
||||
else:
|
||||
ret[c.name] = curr_item
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
class Books(BaseModel):
|
||||
__tablename__ = 'books'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
isbn: str = Column(String(20), nullable=True, index=True)
|
||||
barcode: str = Column(String(50), nullable=True)
|
||||
|
||||
title: str = Column(Text, nullable=False)
|
||||
subtitle: str = Column(Text, nullable=True)
|
||||
author: str = Column(Text, nullable=False)
|
||||
translator: str = Column(Text, nullable=True)
|
||||
editor: str = Column(Text, nullable=True)
|
||||
publisher: str = Column(String(200), nullable=True)
|
||||
publication_date = Column(BigInteger, nullable=True)
|
||||
edition: str = Column(String(50), nullable=True)
|
||||
pages: int = Column(Integer, nullable=True)
|
||||
language: str = Column(String(50), nullable=True)
|
||||
|
||||
category_id: int = Column(Integer, nullable=True, index=True)
|
||||
subject: str = Column(String(200), nullable=True)
|
||||
keywords: str = Column(Text, nullable=True)
|
||||
description: str = Column(Text, nullable=True)
|
||||
abstract: str = Column(Text, nullable=True)
|
||||
|
||||
format: str = Column(String(50), nullable=True)
|
||||
binding: str = Column(String(20), nullable=True)
|
||||
weight: float = Column(Float, nullable=True)
|
||||
cover_image: str = Column(String(500), nullable=True)
|
||||
|
||||
|
||||
def generate_random_book_data(book_id: int, fake: Faker) -> dict:
|
||||
return {
|
||||
'id': book_id,
|
||||
'isbn': fake.isbn13(separator='-'),
|
||||
'barcode': fake.ean(length=13),
|
||||
'title': fake.sentence(nb_words=6),
|
||||
'subtitle': fake.sentence(nb_words=3),
|
||||
'author': fake.name(),
|
||||
'translator': fake.name(),
|
||||
'editor': fake.name(),
|
||||
'publisher': fake.company(),
|
||||
'publication_date': random.randint(-2190472676, 1754206067),
|
||||
'edition': fake.word(),
|
||||
'pages': random.randint(100, 1000),
|
||||
'language': random.choice(['English', 'Français', 'Deutsch']),
|
||||
'category_id': random.randint(1, 100),
|
||||
'subject': fake.word(),
|
||||
'keywords': ", ".join(fake.words(nb=5, unique=True, ext_word_list=None)),
|
||||
'description': fake.text(max_nb_chars=200),
|
||||
'abstract': fake.text(max_nb_chars=100),
|
||||
'format': random.choice(['16mo', '8vo', '4to', 'Folio', 'Quarto']),
|
||||
'binding': random.choice(['expensive', 'cheap', 'paperback', 'hardcover']),
|
||||
'weight': random.uniform(100, 2000),
|
||||
'cover_image': "https://example.com/cover_image.jpg"
|
||||
}
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_db_session():
|
||||
session = SessionLocal()
|
||||
try:
|
||||
yield session
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
print(f"Database error: {e}")
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def check_and_generate_random_data(generate_count=10000):
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
fake = Faker('en_GB')
|
||||
batch_size = 10000
|
||||
|
||||
with get_db_session() as session:
|
||||
data_count = session.query(Books).count()
|
||||
if data_count != 0:
|
||||
if input(
|
||||
f"Database already contains data ({data_count}). Do you want to regenerate random data? (y/n): ").lower() != 'y':
|
||||
return
|
||||
|
||||
if input("Delete all existing data? (y/n): ").lower() == 'y':
|
||||
if data_count > 0:
|
||||
session.query(Books).delete()
|
||||
session.commit()
|
||||
|
||||
for batch_start in tqdm(range(0, generate_count, batch_size), desc="Inserting batches"):
|
||||
batch_end = min(batch_start + batch_size, generate_count)
|
||||
|
||||
batch_data = [
|
||||
generate_random_book_data(i, fake)
|
||||
for i in range(batch_start, batch_end)
|
||||
]
|
||||
|
||||
with get_db_session() as session:
|
||||
session.bulk_insert_mappings(Books, batch_data)
|
||||
session.commit()
|
||||
|
||||
#
|
||||
# with app.app_context():
|
||||
# check_and_generate_random_data()
|
||||
|
||||
# check_and_generate_random_data()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_and_generate_random_data(int(input("Enter the number of random books to generate: ")))
|
||||
print("Database initialized and random data generated.")
|
||||
193
rest_grpc_compare_code/server/grpc/grpc_server.py
Normal file
193
rest_grpc_compare_code/server/grpc/grpc_server.py
Normal file
@ -0,0 +1,193 @@
|
||||
import grpc
|
||||
from concurrent import futures
|
||||
import logging
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from proto import Book_pb2_grpc, Book_pb2
|
||||
from google.protobuf.json_format import MessageToDict
|
||||
from database import Books, get_db_session, Paginator
|
||||
import psutil
|
||||
import time
|
||||
import argparse
|
||||
|
||||
def performance_test(response_object):
|
||||
def decorator(func):
|
||||
def wrapper(self, request, context):
|
||||
response_data = func(self, request, context)
|
||||
test_return = response_object(
|
||||
response_data=response_data
|
||||
)
|
||||
|
||||
process = psutil.Process()
|
||||
# 1. parse request
|
||||
cpu_count = psutil.cpu_count()
|
||||
|
||||
process.cpu_percent()
|
||||
# cpu_start = process.cpu_times()
|
||||
parse_start = time.perf_counter()
|
||||
|
||||
request_deserializer = request.__class__.FromString
|
||||
request_deserializer(request.SerializeToString())
|
||||
|
||||
parse_end = time.perf_counter()
|
||||
cpu_percent_mid = process.cpu_percent()
|
||||
# cpu_mid = process.cpu_times()
|
||||
|
||||
# 2. serialize response
|
||||
serialize_start = time.perf_counter()
|
||||
process.cpu_percent()
|
||||
# cpu_serialize_start = process.cpu_times()
|
||||
|
||||
response_serializer = response_object.SerializeToString
|
||||
response_serializer(test_return)
|
||||
|
||||
serialize_end = time.perf_counter()
|
||||
cpu_end_percent = process.cpu_percent()
|
||||
# cpu_end = process.cpu_times()
|
||||
|
||||
parse_total_time = parse_end - parse_start
|
||||
# parse_cpu_time = (cpu_mid.user - cpu_start.user) + (cpu_mid.system - cpu_start.system)
|
||||
serialize_total_time = serialize_end - serialize_start
|
||||
# serialize_cpu_time = (cpu_end.user - cpu_serialize_start.user) + (cpu_end.system - cpu_serialize_start.system)
|
||||
|
||||
timing_data = {
|
||||
'server_deserialize': {
|
||||
'time': parse_total_time,
|
||||
'cpu': cpu_percent_mid / cpu_count # / parse_total_time * 100
|
||||
},
|
||||
'server_serialize': {
|
||||
'time': serialize_total_time,
|
||||
'cpu': cpu_end_percent / cpu_count # / serialize_total_time * 100
|
||||
},
|
||||
'server_protocol_total_time': parse_end - parse_start + serialize_end - serialize_start
|
||||
}
|
||||
|
||||
return response_object(
|
||||
response_data=response_data,
|
||||
**timing_data
|
||||
)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
class BookServiceServicer(Book_pb2_grpc.BookServiceServicer):
|
||||
|
||||
@performance_test(Book_pb2.GetListResponse)
|
||||
def GetList(self, request, context):
|
||||
# print("request", request)
|
||||
pages = request.pages
|
||||
per_page = request.per_page
|
||||
list_data_limit = request.list_data_limit
|
||||
if list_data_limit <= 0:
|
||||
list_data_limit = per_page
|
||||
|
||||
with get_db_session() as session:
|
||||
paginator = Paginator(session)
|
||||
query = session.query(Books).order_by(Books.id)
|
||||
data = paginator.paginate(query, page=pages, per_page=min(per_page, list_data_limit), error_out=False)
|
||||
books = [book.to_dict() for book in data.items]
|
||||
if len(books) < per_page:
|
||||
books = books * (per_page // len(books)) + books[:per_page % len(books)]
|
||||
|
||||
return books
|
||||
|
||||
@performance_test(Book_pb2.GeneralResponse)
|
||||
def AddBooks(self, request, context):
|
||||
books = request.books
|
||||
test_only = request.test_only
|
||||
if test_only:
|
||||
return Book_pb2.MessageResponse(message="Books added successfully")
|
||||
|
||||
with get_db_session() as session:
|
||||
try:
|
||||
for book in books:
|
||||
book_dict = MessageToDict(book, preserving_proto_field_name=True)
|
||||
if "id" in book_dict:
|
||||
book_dict.pop("id")
|
||||
new_book = Books(**book_dict)
|
||||
session.add(new_book)
|
||||
session.commit()
|
||||
except IntegrityError as e:
|
||||
print(f"Unique constraint violation: {e}, trying to reset sequence")
|
||||
session.rollback()
|
||||
session.execute(text("SELECT setval('books_id_seq', (SELECT COALESCE(MAX(id), 0) FROM books))"))
|
||||
session.commit()
|
||||
|
||||
for book in books:
|
||||
book_dict = MessageToDict(book, preserving_proto_field_name=True)
|
||||
if "id" in book_dict:
|
||||
book_dict.pop("id")
|
||||
new_book = Books(**book_dict)
|
||||
session.add(new_book)
|
||||
session.commit()
|
||||
|
||||
return Book_pb2.MessageResponse(message="Books added successfully")
|
||||
|
||||
@performance_test(Book_pb2.GeneralResponse)
|
||||
def DeleteBooks(self, request, context):
|
||||
book_ids = request.book_ids
|
||||
delete_last_count = request.delete_last_count
|
||||
|
||||
with get_db_session() as session:
|
||||
if delete_last_count > 0:
|
||||
last_ids = session.query(Books.id).order_by(Books.id.desc()).limit(delete_last_count).subquery()
|
||||
session.query(Books).filter(Books.id.in_(
|
||||
session.query(last_ids.c.id)
|
||||
)).delete(synchronize_session=False)
|
||||
else:
|
||||
session.query(Books).filter(Books.id.in_(book_ids)).delete(synchronize_session=False)
|
||||
|
||||
session.commit()
|
||||
|
||||
return Book_pb2.MessageResponse(message="Books deleted successfully")
|
||||
|
||||
@performance_test(Book_pb2.GeneralResponse)
|
||||
def UpdateBook(self, request, context):
|
||||
book_data = request.book
|
||||
with get_db_session() as session:
|
||||
book = session.query(Books).filter(Books.id == book_data.id).first()
|
||||
if book:
|
||||
for key, value in book_data.items():
|
||||
setattr(book, key, value)
|
||||
session.commit()
|
||||
return Book_pb2.MessageResponse(message="Book updated successfully")
|
||||
else:
|
||||
context.set_code(grpc.StatusCode.NOT_FOUND)
|
||||
context.set_details("Book not found")
|
||||
return Book_pb2.MessageResponse(message="Book not found")
|
||||
|
||||
def Ping(self, request, context):
|
||||
return Book_pb2.MessageResponse(message="Pong!")
|
||||
|
||||
|
||||
def serve(port=50051):
|
||||
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10),
|
||||
options=[
|
||||
('grpc.max_receive_message_length', -1),
|
||||
('grpc.max_send_message_length', -1),
|
||||
])
|
||||
|
||||
Book_pb2_grpc.add_BookServiceServicer_to_server(
|
||||
BookServiceServicer(), server
|
||||
)
|
||||
|
||||
listen_addr = f'[::]:{port}'
|
||||
server.add_insecure_port(listen_addr)
|
||||
|
||||
server.start()
|
||||
print(f"gRPC server runs on: {listen_addr}")
|
||||
|
||||
try:
|
||||
server.wait_for_termination()
|
||||
except KeyboardInterrupt:
|
||||
server.stop(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
arg_parse = argparse.ArgumentParser(description="Run REST server")
|
||||
arg_parse.add_argument("--port", "-p", type=int, default=50051, help="Port to run the server on")
|
||||
|
||||
args = arg_parse.parse_args()
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
serve(args.port)
|
||||
83
rest_grpc_compare_code/server/grpc/proto/Book.proto
Normal file
83
rest_grpc_compare_code/server/grpc/proto/Book.proto
Normal file
@ -0,0 +1,83 @@
|
||||
syntax = "proto3";
|
||||
|
||||
service BookService {
|
||||
rpc GetList (GetListRequest) returns (GetListResponse);
|
||||
|
||||
rpc AddBooks (AddBookRequest) returns (GeneralResponse);
|
||||
|
||||
rpc DeleteBooks (DeleteBookRequest) returns (GeneralResponse);
|
||||
|
||||
rpc UpdateBook (UpdateBookRequest) returns (GeneralResponse);
|
||||
|
||||
rpc Ping (Empty) returns (MessageResponse);
|
||||
}
|
||||
|
||||
message Empty {}
|
||||
|
||||
message GetListRequest {
|
||||
int32 pages = 1;
|
||||
int32 per_page = 2;
|
||||
int32 list_data_limit = 3;
|
||||
}
|
||||
|
||||
message AddBookRequest {
|
||||
repeated Book_info books = 1;
|
||||
bool test_only = 2;
|
||||
}
|
||||
|
||||
message DeleteBookRequest {
|
||||
repeated int32 book_ids = 1;
|
||||
int32 delete_last_count = 2;
|
||||
}
|
||||
|
||||
message UpdateBookRequest {
|
||||
Book_info book = 1;
|
||||
}
|
||||
|
||||
message MessageResponse {
|
||||
string message = 1;
|
||||
}
|
||||
|
||||
message GeneralResponse {
|
||||
MessageResponse response_data = 1;
|
||||
Server_deserialize server_deserialize = 2;
|
||||
double server_protocol_total_time = 3;
|
||||
Server_deserialize server_serialize = 4;
|
||||
}
|
||||
|
||||
message GetListResponse {
|
||||
repeated Book_info response_data = 1;
|
||||
Server_deserialize server_deserialize = 2;
|
||||
double server_protocol_total_time = 3;
|
||||
Server_deserialize server_serialize = 4;
|
||||
}
|
||||
|
||||
message Server_deserialize {
|
||||
double cpu = 1;
|
||||
double time = 2;
|
||||
}
|
||||
|
||||
message Book_info {
|
||||
string abstract = 1;
|
||||
string author = 2;
|
||||
string barcode = 3;
|
||||
string binding = 4;
|
||||
int32 category_id = 5;
|
||||
string cover_image = 6;
|
||||
string description = 7;
|
||||
string edition = 8;
|
||||
string editor = 9;
|
||||
string format = 10;
|
||||
int32 id = 11;
|
||||
string isbn = 12;
|
||||
string keywords = 13;
|
||||
string language = 14;
|
||||
int32 pages = 15;
|
||||
int64 publication_date = 16;
|
||||
string publisher = 17;
|
||||
string subject = 18;
|
||||
string subtitle = 19;
|
||||
string title = 20;
|
||||
string translator = 21;
|
||||
double weight = 22;
|
||||
}
|
||||
56
rest_grpc_compare_code/server/grpc/proto/Book_pb2.py
Normal file
56
rest_grpc_compare_code/server/grpc/proto/Book_pb2.py
Normal file
@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# NO CHECKED-IN PROTOBUF GENCODE
|
||||
# source: Book.proto
|
||||
# Protobuf Python Version: 6.31.1
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import runtime_version as _runtime_version
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
from google.protobuf.internal import builder as _builder
|
||||
_runtime_version.ValidateProtobufRuntimeVersion(
|
||||
_runtime_version.Domain.PUBLIC,
|
||||
6,
|
||||
31,
|
||||
1,
|
||||
'',
|
||||
'Book.proto'
|
||||
)
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nBook.proto\"\x07\n\x05\x45mpty\"J\n\x0eGetListRequest\x12\r\n\x05pages\x18\x01 \x01(\x05\x12\x10\n\x08per_page\x18\x02 \x01(\x05\x12\x17\n\x0flist_data_limit\x18\x03 \x01(\x05\">\n\x0e\x41\x64\x64\x42ookRequest\x12\x19\n\x05\x62ooks\x18\x01 \x03(\x0b\x32\n.Book_info\x12\x11\n\ttest_only\x18\x02 \x01(\x08\"@\n\x11\x44\x65leteBookRequest\x12\x10\n\x08\x62ook_ids\x18\x01 \x03(\x05\x12\x19\n\x11\x64\x65lete_last_count\x18\x02 \x01(\x05\"-\n\x11UpdateBookRequest\x12\x18\n\x04\x62ook\x18\x01 \x01(\x0b\x32\n.Book_info\"\"\n\x0fMessageResponse\x12\x0f\n\x07message\x18\x01 \x01(\t\"\xbe\x01\n\x0fGeneralResponse\x12\'\n\rresponse_data\x18\x01 \x01(\x0b\x32\x10.MessageResponse\x12/\n\x12server_deserialize\x18\x02 \x01(\x0b\x32\x13.Server_deserialize\x12\"\n\x1aserver_protocol_total_time\x18\x03 \x01(\x01\x12-\n\x10server_serialize\x18\x04 \x01(\x0b\x32\x13.Server_deserialize\"\xb8\x01\n\x0fGetListResponse\x12!\n\rresponse_data\x18\x01 \x03(\x0b\x32\n.Book_info\x12/\n\x12server_deserialize\x18\x02 \x01(\x0b\x32\x13.Server_deserialize\x12\"\n\x1aserver_protocol_total_time\x18\x03 \x01(\x01\x12-\n\x10server_serialize\x18\x04 \x01(\x0b\x32\x13.Server_deserialize\"/\n\x12Server_deserialize\x12\x0b\n\x03\x63pu\x18\x01 \x01(\x01\x12\x0c\n\x04time\x18\x02 \x01(\x01\"\x8f\x03\n\tBook_info\x12\x10\n\x08\x61\x62stract\x18\x01 \x01(\t\x12\x0e\n\x06\x61uthor\x18\x02 \x01(\t\x12\x0f\n\x07\x62\x61rcode\x18\x03 \x01(\t\x12\x0f\n\x07\x62inding\x18\x04 \x01(\t\x12\x13\n\x0b\x63\x61tegory_id\x18\x05 \x01(\x05\x12\x13\n\x0b\x63over_image\x18\x06 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x07 \x01(\t\x12\x0f\n\x07\x65\x64ition\x18\x08 \x01(\t\x12\x0e\n\x06\x65\x64itor\x18\t \x01(\t\x12\x0e\n\x06\x66ormat\x18\n \x01(\t\x12\n\n\x02id\x18\x0b \x01(\x05\x12\x0c\n\x04isbn\x18\x0c \x01(\t\x12\x10\n\x08keywords\x18\r \x01(\t\x12\x10\n\x08language\x18\x0e \x01(\t\x12\r\n\x05pages\x18\x0f \x01(\x05\x12\x18\n\x10publication_date\x18\x10 \x01(\x03\x12\x11\n\tpublisher\x18\x11 \x01(\t\x12\x0f\n\x07subject\x18\x12 \x01(\t\x12\x10\n\x08subtitle\x18\x13 \x01(\t\x12\r\n\x05title\x18\x14 \x01(\t\x12\x12\n\ntranslator\x18\x15 \x01(\t\x12\x0e\n\x06weight\x18\x16 \x01(\x01\x32\xf5\x01\n\x0b\x42ookService\x12,\n\x07GetList\x12\x0f.GetListRequest\x1a\x10.GetListResponse\x12-\n\x08\x41\x64\x64\x42ooks\x12\x0f.AddBookRequest\x1a\x10.GeneralResponse\x12\x33\n\x0b\x44\x65leteBooks\x12\x12.DeleteBookRequest\x1a\x10.GeneralResponse\x12\x32\n\nUpdateBook\x12\x12.UpdateBookRequest\x1a\x10.GeneralResponse\x12 \n\x04Ping\x12\x06.Empty\x1a\x10.MessageResponseb\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'Book_pb2', _globals)
|
||||
if not _descriptor._USE_C_DESCRIPTORS:
|
||||
DESCRIPTOR._loaded_options = None
|
||||
_globals['_EMPTY']._serialized_start=14
|
||||
_globals['_EMPTY']._serialized_end=21
|
||||
_globals['_GETLISTREQUEST']._serialized_start=23
|
||||
_globals['_GETLISTREQUEST']._serialized_end=97
|
||||
_globals['_ADDBOOKREQUEST']._serialized_start=99
|
||||
_globals['_ADDBOOKREQUEST']._serialized_end=161
|
||||
_globals['_DELETEBOOKREQUEST']._serialized_start=163
|
||||
_globals['_DELETEBOOKREQUEST']._serialized_end=227
|
||||
_globals['_UPDATEBOOKREQUEST']._serialized_start=229
|
||||
_globals['_UPDATEBOOKREQUEST']._serialized_end=274
|
||||
_globals['_MESSAGERESPONSE']._serialized_start=276
|
||||
_globals['_MESSAGERESPONSE']._serialized_end=310
|
||||
_globals['_GENERALRESPONSE']._serialized_start=313
|
||||
_globals['_GENERALRESPONSE']._serialized_end=503
|
||||
_globals['_GETLISTRESPONSE']._serialized_start=506
|
||||
_globals['_GETLISTRESPONSE']._serialized_end=690
|
||||
_globals['_SERVER_DESERIALIZE']._serialized_start=692
|
||||
_globals['_SERVER_DESERIALIZE']._serialized_end=739
|
||||
_globals['_BOOK_INFO']._serialized_start=742
|
||||
_globals['_BOOK_INFO']._serialized_end=1141
|
||||
_globals['_BOOKSERVICE']._serialized_start=1144
|
||||
_globals['_BOOKSERVICE']._serialized_end=1389
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
269
rest_grpc_compare_code/server/grpc/proto/Book_pb2_grpc.py
Normal file
269
rest_grpc_compare_code/server/grpc/proto/Book_pb2_grpc.py
Normal file
@ -0,0 +1,269 @@
|
||||
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||
"""Client and server classes corresponding to protobuf-defined services."""
|
||||
import grpc
|
||||
import warnings
|
||||
|
||||
from . import Book_pb2 as Book__pb2
|
||||
|
||||
GRPC_GENERATED_VERSION = '1.74.0'
|
||||
GRPC_VERSION = grpc.__version__
|
||||
_version_not_supported = False
|
||||
|
||||
try:
|
||||
from grpc._utilities import first_version_is_lower
|
||||
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
|
||||
except ImportError:
|
||||
_version_not_supported = True
|
||||
|
||||
if _version_not_supported:
|
||||
raise RuntimeError(
|
||||
f'The grpc package installed is at version {GRPC_VERSION},'
|
||||
+ f' but the generated code in Book_pb2_grpc.py depends on'
|
||||
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
|
||||
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
|
||||
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
|
||||
)
|
||||
|
||||
|
||||
class BookServiceStub(object):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
def __init__(self, channel):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
channel: A grpc.Channel.
|
||||
"""
|
||||
self.GetList = channel.unary_unary(
|
||||
'/BookService/GetList',
|
||||
request_serializer=Book__pb2.GetListRequest.SerializeToString,
|
||||
response_deserializer=Book__pb2.GetListResponse.FromString,
|
||||
_registered_method=True)
|
||||
self.AddBooks = channel.unary_unary(
|
||||
'/BookService/AddBooks',
|
||||
request_serializer=Book__pb2.AddBookRequest.SerializeToString,
|
||||
response_deserializer=Book__pb2.GeneralResponse.FromString,
|
||||
_registered_method=True)
|
||||
self.DeleteBooks = channel.unary_unary(
|
||||
'/BookService/DeleteBooks',
|
||||
request_serializer=Book__pb2.DeleteBookRequest.SerializeToString,
|
||||
response_deserializer=Book__pb2.GeneralResponse.FromString,
|
||||
_registered_method=True)
|
||||
self.UpdateBook = channel.unary_unary(
|
||||
'/BookService/UpdateBook',
|
||||
request_serializer=Book__pb2.UpdateBookRequest.SerializeToString,
|
||||
response_deserializer=Book__pb2.GeneralResponse.FromString,
|
||||
_registered_method=True)
|
||||
self.Ping = channel.unary_unary(
|
||||
'/BookService/Ping',
|
||||
request_serializer=Book__pb2.Empty.SerializeToString,
|
||||
response_deserializer=Book__pb2.MessageResponse.FromString,
|
||||
_registered_method=True)
|
||||
|
||||
|
||||
class BookServiceServicer(object):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
def GetList(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def AddBooks(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def DeleteBooks(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def UpdateBook(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def Ping(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
|
||||
def add_BookServiceServicer_to_server(servicer, server):
|
||||
rpc_method_handlers = {
|
||||
'GetList': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.GetList,
|
||||
request_deserializer=Book__pb2.GetListRequest.FromString,
|
||||
response_serializer=Book__pb2.GetListResponse.SerializeToString,
|
||||
),
|
||||
'AddBooks': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.AddBooks,
|
||||
request_deserializer=Book__pb2.AddBookRequest.FromString,
|
||||
response_serializer=Book__pb2.GeneralResponse.SerializeToString,
|
||||
),
|
||||
'DeleteBooks': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.DeleteBooks,
|
||||
request_deserializer=Book__pb2.DeleteBookRequest.FromString,
|
||||
response_serializer=Book__pb2.GeneralResponse.SerializeToString,
|
||||
),
|
||||
'UpdateBook': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.UpdateBook,
|
||||
request_deserializer=Book__pb2.UpdateBookRequest.FromString,
|
||||
response_serializer=Book__pb2.GeneralResponse.SerializeToString,
|
||||
),
|
||||
'Ping': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.Ping,
|
||||
request_deserializer=Book__pb2.Empty.FromString,
|
||||
response_serializer=Book__pb2.MessageResponse.SerializeToString,
|
||||
),
|
||||
}
|
||||
generic_handler = grpc.method_handlers_generic_handler(
|
||||
'BookService', rpc_method_handlers)
|
||||
server.add_generic_rpc_handlers((generic_handler,))
|
||||
server.add_registered_method_handlers('BookService', rpc_method_handlers)
|
||||
|
||||
|
||||
# This class is part of an EXPERIMENTAL API.
|
||||
class BookService(object):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
@staticmethod
|
||||
def GetList(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/BookService/GetList',
|
||||
Book__pb2.GetListRequest.SerializeToString,
|
||||
Book__pb2.GetListResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def AddBooks(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/BookService/AddBooks',
|
||||
Book__pb2.AddBookRequest.SerializeToString,
|
||||
Book__pb2.GeneralResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def DeleteBooks(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/BookService/DeleteBooks',
|
||||
Book__pb2.DeleteBookRequest.SerializeToString,
|
||||
Book__pb2.GeneralResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def UpdateBook(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/BookService/UpdateBook',
|
||||
Book__pb2.UpdateBookRequest.SerializeToString,
|
||||
Book__pb2.GeneralResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def Ping(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/BookService/Ping',
|
||||
Book__pb2.Empty.SerializeToString,
|
||||
Book__pb2.MessageResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
1
rest_grpc_compare_code/server/grpc/proto/__init__.py
Normal file
1
rest_grpc_compare_code/server/grpc/proto/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import Book_pb2, Book_pb2_grpc
|
||||
1
rest_grpc_compare_code/server/grpc/proto/generate_pb.bat
Normal file
1
rest_grpc_compare_code/server/grpc/proto/generate_pb.bat
Normal file
@ -0,0 +1 @@
|
||||
py -m grpc_tools.protoc --python_out=. --grpc_python_out=. --proto_path=. Book.proto
|
||||
BIN
rest_grpc_compare_code/server/grpc/rest_server_books.db
Normal file
BIN
rest_grpc_compare_code/server/grpc/rest_server_books.db
Normal file
Binary file not shown.
111
rest_grpc_compare_code/server/rest/database.py
Normal file
111
rest_grpc_compare_code/server/rest/database.py
Normal file
@ -0,0 +1,111 @@
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask import Flask
|
||||
from flask_cors import CORS
|
||||
from enum import Enum
|
||||
from faker import Faker
|
||||
import random
|
||||
|
||||
app = Flask(__name__)
|
||||
# app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///rest_server_books.db'
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://postgres:liuyanfeng66@localhost:5432/BookRestGrpcCompare'
|
||||
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
|
||||
'pool_size': 30, # 主连接数
|
||||
'max_overflow': 50, # 额外连接数
|
||||
'pool_pre_ping': True, # 检测失效连接,自动重连
|
||||
# 'connect_args': {
|
||||
# 'check_same_thread': False,
|
||||
# 'timeout': 30 # 文件锁最长等待秒数,视情况可调
|
||||
# }
|
||||
}
|
||||
CORS(app, supports_credentials=True)
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
|
||||
class BaseModel(db.Model):
|
||||
__abstract__ = True
|
||||
|
||||
def to_dict(self, exclude: set = None):
|
||||
exclude = exclude or set()
|
||||
ret = {}
|
||||
for c in self.__table__.columns:
|
||||
if c.name not in exclude:
|
||||
curr_item = getattr(self, c.name)
|
||||
if isinstance(curr_item, Enum):
|
||||
ret[c.name] = curr_item.value
|
||||
else:
|
||||
ret[c.name] = curr_item
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
class Books(BaseModel):
|
||||
__tablename__ = 'books'
|
||||
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
|
||||
isbn: str = db.Column(db.String(20), nullable=True, index=True)
|
||||
barcode: str = db.Column(db.String(50), nullable=True)
|
||||
|
||||
title: str = db.Column(db.Text, nullable=False)
|
||||
subtitle: str = db.Column(db.Text, nullable=True)
|
||||
author: str = db.Column(db.Text, nullable=False)
|
||||
translator: str = db.Column(db.Text, nullable=True)
|
||||
editor: str = db.Column(db.Text, nullable=True)
|
||||
publisher: str = db.Column(db.String(200), nullable=True)
|
||||
publication_date = db.Column(db.BigInteger, nullable=True)
|
||||
edition: str = db.Column(db.String(50), nullable=True)
|
||||
pages: int = db.Column(db.Integer, nullable=True)
|
||||
language: str = db.Column(db.String(50), nullable=True)
|
||||
|
||||
category_id: int = db.Column(db.Integer, nullable=True, index=True)
|
||||
subject: str = db.Column(db.String(200), nullable=True)
|
||||
keywords: str = db.Column(db.Text, nullable=True)
|
||||
description: str = db.Column(db.Text, nullable=True)
|
||||
abstract: str = db.Column(db.Text, nullable=True)
|
||||
|
||||
format: str = db.Column(db.String(50), nullable=True)
|
||||
binding: str = db.Column(db.String(20), nullable=True)
|
||||
weight: float = db.Column(db.Float, nullable=True)
|
||||
cover_image: str = db.Column(db.String(500), nullable=True)
|
||||
|
||||
|
||||
def generate_random_book_data(book_id: int, fake: Faker) -> dict:
|
||||
return {
|
||||
'id': book_id,
|
||||
'isbn': fake.isbn13(separator='-'),
|
||||
'barcode': fake.ean(length=13),
|
||||
'title': fake.sentence(nb_words=6),
|
||||
'subtitle': fake.sentence(nb_words=3),
|
||||
'author': fake.name(),
|
||||
'translator': fake.name(),
|
||||
'editor': fake.name(),
|
||||
'publisher': fake.company(),
|
||||
'publication_date': random.randint(-2190472676, 1754206067),
|
||||
'edition': fake.word(),
|
||||
'pages': random.randint(100, 1000),
|
||||
'language': random.choice(['English', 'Français', 'Deutsch']),
|
||||
'category_id': random.randint(1, 100),
|
||||
'subject': fake.word(),
|
||||
'keywords': ", ".join(fake.words(nb=5, unique=True, ext_word_list=None)),
|
||||
'description': fake.text(max_nb_chars=200),
|
||||
'abstract': fake.text(max_nb_chars=100),
|
||||
'format': random.choice(['16mo', '8vo', '4to', 'Folio', 'Quarto']),
|
||||
'binding': random.choice(['expensive', 'cheap', 'paperback', 'hardcover']),
|
||||
'weight': random.uniform(100, 2000),
|
||||
'cover_image': "https://example.com/cover_image.jpg"
|
||||
}
|
||||
|
||||
def check_and_generate_random_data():
|
||||
db.create_all()
|
||||
|
||||
if Books.query.count() == 0:
|
||||
fake = Faker('en_GB')
|
||||
for i in range(10000):
|
||||
book = Books(
|
||||
**generate_random_book_data(i, fake)
|
||||
)
|
||||
db.session.add(book)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
# with app.app_context():
|
||||
# check_and_generate_random_data()
|
||||
BIN
rest_grpc_compare_code/server/rest/instance/rest_server_books.db
Normal file
BIN
rest_grpc_compare_code/server/rest/instance/rest_server_books.db
Normal file
Binary file not shown.
150
rest_grpc_compare_code/server/rest/server_rest.py
Normal file
150
rest_grpc_compare_code/server/rest/server_rest.py
Normal file
@ -0,0 +1,150 @@
|
||||
from flask import Flask, request, jsonify
|
||||
import time
|
||||
import psutil
|
||||
from database import app, db, Books
|
||||
import argparse
|
||||
|
||||
def performance_test(is_get=False):
|
||||
def decorator(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
process = psutil.Process()
|
||||
cpu_count = psutil.cpu_count()
|
||||
|
||||
# 1. parse request
|
||||
process.cpu_percent()
|
||||
# cpu_start = process.cpu_times()
|
||||
parse_start = time.perf_counter()
|
||||
|
||||
if is_get:
|
||||
data = request.args.to_dict()
|
||||
else:
|
||||
data = request.get_json()
|
||||
|
||||
parse_end = time.perf_counter()
|
||||
cpu_percent_mid = process.cpu_percent()
|
||||
# cpu_mid = process.cpu_times()
|
||||
|
||||
# process
|
||||
result = func(*args, **kwargs, request_data=data)
|
||||
|
||||
# 2. serialize response
|
||||
serialize_start = time.perf_counter()
|
||||
process.cpu_percent()
|
||||
# cpu_serialize_start = process.cpu_times()
|
||||
|
||||
response_data = jsonify(result)
|
||||
|
||||
serialize_end = time.perf_counter()
|
||||
cpu_end_percent = process.cpu_percent()
|
||||
# cpu_end = process.cpu_times()
|
||||
|
||||
parse_time = parse_end - parse_start
|
||||
# parse_cpu_time = (cpu_mid.user - cpu_start.user) + (cpu_mid.system - cpu_start.system)
|
||||
|
||||
server_serialize_time = serialize_end - serialize_start
|
||||
# server_serialize_cpu_time = (cpu_end.user - cpu_serialize_start.user) + (cpu_end.system - cpu_serialize_start.system)
|
||||
|
||||
# timing
|
||||
timing_data = {
|
||||
'server_deserialize': {
|
||||
'time': parse_time,
|
||||
'cpu': cpu_percent_mid / cpu_count # / parse_time * 100
|
||||
},
|
||||
'server_serialize': {
|
||||
'time': server_serialize_time,
|
||||
'cpu': cpu_end_percent / cpu_count # / server_serialize_time * 100
|
||||
},
|
||||
'server_protocol_total_time': parse_end - parse_start + serialize_end - serialize_start,
|
||||
'response_data': result,
|
||||
}
|
||||
|
||||
# print(f"Server timing: {timing_data}")
|
||||
return jsonify(timing_data), response_data.status_code
|
||||
|
||||
wrapper.__name__ = func.__name__
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
@app.route('/api/get_list', methods=['GET'])
|
||||
@performance_test(is_get=True)
|
||||
def get_list(request_data):
|
||||
page_count = int(request_data.get("pages", 1))
|
||||
per_page = int(request_data.get("per_page", 100))
|
||||
list_data_limit = int(request_data.get("list_data_limit", per_page))
|
||||
if list_data_limit <= 0:
|
||||
list_data_limit = per_page
|
||||
|
||||
if per_page <= 0:
|
||||
return []
|
||||
|
||||
data = Books.query.order_by(Books.id).paginate(page=page_count, per_page=min(per_page, list_data_limit), error_out=False)
|
||||
books = [book.to_dict() for book in data.items]
|
||||
if len(books) < per_page:
|
||||
books = books * (per_page // len(books)) + books[:per_page % len(books)]
|
||||
|
||||
return books
|
||||
|
||||
|
||||
@app.route('/api/add_book', methods=['POST'])
|
||||
@performance_test(is_get=False)
|
||||
def add_book(request_data):
|
||||
books_data = request_data.get('books', [])
|
||||
test_only = request_data.get('test_only', False)
|
||||
if test_only:
|
||||
return {"message": "Books added successfully"}
|
||||
|
||||
for book in books_data:
|
||||
new_book = Books(**book)
|
||||
db.session.add(new_book)
|
||||
db.session.commit()
|
||||
return {"message": "Books added successfully"}
|
||||
|
||||
|
||||
@app.route('/api/delete_books', methods=['DELETE'])
|
||||
@performance_test(is_get=False)
|
||||
def delete_books(request_data):
|
||||
book_ids = request_data.get('book_ids', [])
|
||||
delete_last_count = request_data.get('delete_last_count', -1)
|
||||
|
||||
if delete_last_count > 0:
|
||||
last_ids = db.session.query(Books.id).order_by(Books.id.desc()).limit(delete_last_count).subquery()
|
||||
Books.query.filter(Books.id.in_(
|
||||
db.session.query(last_ids.c.id)
|
||||
)).delete(synchronize_session=False)
|
||||
else:
|
||||
Books.query.filter(Books.id.in_(book_ids)).delete(synchronize_session=False)
|
||||
|
||||
db.session.commit()
|
||||
return {"message": "Books deleted successfully"}
|
||||
|
||||
|
||||
@app.route('/api/update_book', methods=['PUT'])
|
||||
@performance_test(is_get=False)
|
||||
def update_book(request_data):
|
||||
book_data = request_data.get('book', {})
|
||||
book_id = book_data.get('id')
|
||||
|
||||
book = db.session.get(Books, book_id)
|
||||
if not book:
|
||||
return {"message": "Book not found"}
|
||||
|
||||
for key, value in book_data.items():
|
||||
if key == 'id':
|
||||
continue
|
||||
setattr(book, key, value)
|
||||
|
||||
db.session.commit()
|
||||
return {"message": "Book updated successfully"}
|
||||
|
||||
@app.route('/api/ping', methods=['GET'])
|
||||
def ping():
|
||||
return jsonify({"message": "pong"})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
arg_parse = argparse.ArgumentParser(description="Run REST server")
|
||||
arg_parse.add_argument("--port", "-p", type=int, default=9500, help="Port to run the server on")
|
||||
|
||||
args = arg_parse.parse_args()
|
||||
app.run("0.0.0.0", port=args.port)
|
||||
46
rest_grpc_compare_code/utils.py
Normal file
46
rest_grpc_compare_code/utils.py
Normal file
@ -0,0 +1,46 @@
|
||||
from dataclasses import dataclass
|
||||
import random
|
||||
from faker import Faker
|
||||
|
||||
@dataclass
|
||||
class NetworkingSize:
|
||||
request_size: int = 0
|
||||
response_size: int = 0
|
||||
|
||||
|
||||
def get_size_from_dict(data: dict):
|
||||
return NetworkingSize(
|
||||
request_size=data.get('request_bytes', 0),
|
||||
response_size=data.get('response_bytes', 0)
|
||||
)
|
||||
|
||||
|
||||
def generate_random_book_data(count: int) -> list[dict]:
|
||||
fake = Faker("en_GB")
|
||||
|
||||
return [
|
||||
{
|
||||
# 'id': book_id,
|
||||
'isbn': fake.isbn13(separator='-'),
|
||||
'barcode': fake.ean(length=13),
|
||||
'title': fake.sentence(nb_words=6),
|
||||
'subtitle': fake.sentence(nb_words=3),
|
||||
'author': fake.name(),
|
||||
'translator': fake.name(),
|
||||
'editor': fake.name(),
|
||||
'publisher': fake.company(),
|
||||
'publication_date': random.randint(-2190472676, 1754206067),
|
||||
'edition': fake.word(),
|
||||
'pages': random.randint(100, 1000),
|
||||
'language': random.choice(['English', 'Français', 'Deutsch']),
|
||||
'category_id': random.randint(1, 100),
|
||||
'subject': fake.word(),
|
||||
'keywords': ", ".join(fake.words(nb=5, unique=True, ext_word_list=None)),
|
||||
'description': fake.text(max_nb_chars=200),
|
||||
'abstract': fake.text(max_nb_chars=100),
|
||||
'format': random.choice(['16mo', '8vo', '4to', 'Folio', 'Quarto']),
|
||||
'binding': random.choice(['expensive', 'cheap', 'paperback', 'hardcover']),
|
||||
'weight': random.uniform(100, 2000),
|
||||
'cover_image': "https://example.com/cover_image.jpg"
|
||||
} for _ in range(count)
|
||||
]
|
||||
4
rest_grpc_compare_frontend/.dockerignore
Normal file
4
rest_grpc_compare_frontend/.dockerignore
Normal file
@ -0,0 +1,4 @@
|
||||
.react-router
|
||||
build
|
||||
node_modules
|
||||
README.md
|
||||
1
rest_grpc_compare_frontend/.env
Normal file
1
rest_grpc_compare_frontend/.env
Normal file
@ -0,0 +1 @@
|
||||
VITE_API_ENDPOINT = "http://127.0.0.1:9520"
|
||||
6
rest_grpc_compare_frontend/.gitignore
vendored
Normal file
6
rest_grpc_compare_frontend/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
.DS_Store
|
||||
/node_modules/
|
||||
|
||||
# React Router
|
||||
/.react-router/
|
||||
/build/
|
||||
22
rest_grpc_compare_frontend/Dockerfile
Normal file
22
rest_grpc_compare_frontend/Dockerfile
Normal file
@ -0,0 +1,22 @@
|
||||
FROM node:20-alpine AS development-dependencies-env
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
RUN npm ci
|
||||
|
||||
FROM node:20-alpine AS production-dependencies-env
|
||||
COPY ./package.json package-lock.json /app/
|
||||
WORKDIR /app
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
FROM node:20-alpine AS build-env
|
||||
COPY . /app/
|
||||
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
|
||||
WORKDIR /app
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20-alpine
|
||||
COPY ./package.json package-lock.json /app/
|
||||
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
|
||||
COPY --from=build-env /app/build /app/build
|
||||
WORKDIR /app
|
||||
CMD ["npm", "run", "start"]
|
||||
87
rest_grpc_compare_frontend/README.md
Normal file
87
rest_grpc_compare_frontend/README.md
Normal file
@ -0,0 +1,87 @@
|
||||
# Welcome to React Router!
|
||||
|
||||
A modern, production-ready template for building full-stack React applications using React Router.
|
||||
|
||||
[](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default)
|
||||
|
||||
## Features
|
||||
|
||||
- 🚀 Server-side rendering
|
||||
- ⚡️ Hot Module Replacement (HMR)
|
||||
- 📦 Asset bundling and optimization
|
||||
- 🔄 Data loading and mutations
|
||||
- 🔒 TypeScript by default
|
||||
- 🎉 TailwindCSS for styling
|
||||
- 📖 [React Router docs](https://reactrouter.com/)
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Installation
|
||||
|
||||
Install the dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
Start the development server with HMR:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Your application will be available at `http://localhost:5173`.
|
||||
|
||||
## Building for Production
|
||||
|
||||
Create a production build:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
To build and run using Docker:
|
||||
|
||||
```bash
|
||||
docker build -t my-app .
|
||||
|
||||
# Run the container
|
||||
docker run -p 3000:3000 my-app
|
||||
```
|
||||
|
||||
The containerized application can be deployed to any platform that supports Docker, including:
|
||||
|
||||
- AWS ECS
|
||||
- Google Cloud Run
|
||||
- Azure Container Apps
|
||||
- Digital Ocean App Platform
|
||||
- Fly.io
|
||||
- Railway
|
||||
|
||||
### DIY Deployment
|
||||
|
||||
If you're familiar with deploying Node applications, the built-in app server is production-ready.
|
||||
|
||||
Make sure to deploy the output of `npm run build`
|
||||
|
||||
```
|
||||
├── package.json
|
||||
├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)
|
||||
├── build/
|
||||
│ ├── client/ # Static assets
|
||||
│ └── server/ # Server-side code
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
|
||||
|
||||
---
|
||||
|
||||
Built with ❤️ using React Router.
|
||||
15
rest_grpc_compare_frontend/app/app.css
Normal file
15
rest_grpc_compare_frontend/app/app.css
Normal file
@ -0,0 +1,15 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
@apply bg-white dark:bg-gray-950;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
250
rest_grpc_compare_frontend/app/components/Charts.tsx
Normal file
250
rest_grpc_compare_frontend/app/components/Charts.tsx
Normal file
@ -0,0 +1,250 @@
|
||||
import {Affix, Button, Divider, Grid, Text, rem, Stack} from "@mantine/core";
|
||||
import { AreaChart } from '@mantine/charts';
|
||||
import type {PerformanceGetListData} from "~/utils/models.ts";
|
||||
import React from "react";
|
||||
|
||||
|
||||
export default function ({data, gridSpan, hideServerDeserialize, hideServerSerialize}: {
|
||||
data: PerformanceGetListData[], gridSpan: number, hideServerDeserialize: boolean, hideServerSerialize: boolean
|
||||
}) {
|
||||
const chartData = data.map(item => ({
|
||||
RequestCount: item.request_count,
|
||||
gRpcClientPing: item.grpc.client_networking_ping,
|
||||
gRpcClientCPU: item.grpc.client_request_cpu,
|
||||
gRpcTimeTotal: item.grpc.client_request_time,
|
||||
gRpcRequestSize: item.grpc.networking_size.request_size,
|
||||
gRpcResponseSize: item.grpc.networking_size.response_size,
|
||||
gRpcServerDeserializeCPU: item.grpc.server_deserialize_cpu,
|
||||
gRpcServerDeserializeTime: item.grpc.server_deserialize_time,
|
||||
gRpcServerSerializeCPU: item.grpc.server_serialize_cpu,
|
||||
gRpcServerSerializeTime: item.grpc.server_serialize_time,
|
||||
gRpcServerProtocolTotalTime: item.grpc.server_protocol_total_time,
|
||||
|
||||
RestClientPing: item.rest.client_networking_ping,
|
||||
RestClientCPU: item.rest.client_request_cpu,
|
||||
RestTimeTotal: item.rest.client_request_time,
|
||||
RestRequestSize: item.rest.networking_size.request_size,
|
||||
RestResponseSize: item.rest.networking_size.response_size,
|
||||
RestServerDeserializeCPU: item.rest.server_deserialize_cpu,
|
||||
RestServerDeserializeTime: item.rest.server_deserialize_time,
|
||||
RestServerSerializeCPU: item.rest.server_serialize_cpu,
|
||||
RestServerSerializeTime: item.rest.server_serialize_time,
|
||||
RestServerProtocolTotalTime: item.rest.server_protocol_total_time,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Grid gutter="xs" grow>
|
||||
<Grid.Col span={gridSpan} mb="md">
|
||||
<Text mb="md" pl="md" ta="center" fw={700}>Client CPU Usage</Text>
|
||||
<AreaChart
|
||||
h={300}
|
||||
data={chartData}
|
||||
dataKey="RequestCount"
|
||||
type="stacked"
|
||||
xAxisLabel="Data Count"
|
||||
yAxisLabel="CPU Usage (%)"
|
||||
withLegend
|
||||
withPointLabels={false}
|
||||
withDots={false}
|
||||
valueFormatter={(value) => new Intl.NumberFormat('en-US').format(value)}
|
||||
areaChartProps={{ syncId: 'RequestCount' }}
|
||||
series={[
|
||||
{ name: 'gRpcClientCPU', label: 'gRPC', color: 'teal.6' },
|
||||
{ name: 'RestClientCPU', label: 'Rest', color: 'indigo.6' },
|
||||
]}
|
||||
/>
|
||||
<Divider my="0px" labelPosition="center" />
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={gridSpan} mb="md">
|
||||
<Text mb="md" pl="md" ta="center" fw={700}>Client Total Time (s)</Text>
|
||||
<AreaChart
|
||||
h={300}
|
||||
data={chartData}
|
||||
dataKey="RequestCount"
|
||||
type="stacked"
|
||||
xAxisLabel="Data Count"
|
||||
yAxisLabel="Total Time (s)"
|
||||
withLegend
|
||||
withPointLabels={false}
|
||||
withDots={false}
|
||||
valueFormatter={(value) => new Intl.NumberFormat('en-GB').format(value)}
|
||||
areaChartProps={{ syncId: 'RequestCount' }}
|
||||
series={[
|
||||
{ name: 'gRpcTimeTotal', label: 'gRPC', color: 'teal.6' },
|
||||
{ name: 'RestTimeTotal', label: 'Rest', color: 'indigo.6' },
|
||||
]}
|
||||
/>
|
||||
<Divider my="0px" labelPosition="center" />
|
||||
</Grid.Col>
|
||||
|
||||
{!hideServerDeserialize &&
|
||||
<Grid.Col span={gridSpan} mb="md">
|
||||
<Text mb="md" pl="md" ta="center" fw={700}>Networking Request Data Size (KB)</Text>
|
||||
<AreaChart
|
||||
h={300}
|
||||
data={chartData}
|
||||
dataKey="RequestCount"
|
||||
type="stacked"
|
||||
xAxisLabel="Data Count"
|
||||
yAxisLabel="Request Data Size (KB)"
|
||||
withLegend
|
||||
withPointLabels={false}
|
||||
withDots={false}
|
||||
valueFormatter={(value) => new Intl.NumberFormat('en-GB').format(value / 1024)}
|
||||
areaChartProps={{ syncId: 'RequestCount' }}
|
||||
series={[
|
||||
{ name: 'gRpcRequestSize', label: 'gRPC', color: 'teal.6' },
|
||||
{ name: 'RestRequestSize', label: 'Rest', color: 'indigo.6' },
|
||||
]}
|
||||
/>
|
||||
<Divider my="0px" labelPosition="center" />
|
||||
</Grid.Col>
|
||||
}
|
||||
|
||||
{!hideServerSerialize &&
|
||||
<Grid.Col span={gridSpan} mb="md">
|
||||
<Text mb="md" pl="md" ta="center" fw={700}>Networking Response Data Size (KB)</Text>
|
||||
<AreaChart
|
||||
h={300}
|
||||
data={chartData}
|
||||
dataKey="RequestCount"
|
||||
type="stacked"
|
||||
xAxisLabel="Data Count"
|
||||
yAxisLabel="Response Data Size (KB)"
|
||||
withLegend
|
||||
withPointLabels={false}
|
||||
withDots={false}
|
||||
valueFormatter={(value) => new Intl.NumberFormat('en-GB').format(value / 1024)}
|
||||
areaChartProps={{ syncId: 'RequestCount' }}
|
||||
series={[
|
||||
{ name: 'gRpcResponseSize', label: 'gRPC', color: 'teal.6' },
|
||||
{ name: 'RestResponseSize', label: 'Rest', color: 'indigo.6' },
|
||||
]}
|
||||
/>
|
||||
<Divider my="0px" labelPosition="center" />
|
||||
</Grid.Col>
|
||||
}
|
||||
|
||||
|
||||
{!hideServerDeserialize &&
|
||||
<Grid.Col span={gridSpan} mb="md">
|
||||
<Text mb="md" pl="md" ta="center" fw={700}>Server Deserialize CPU Usage (%)</Text>
|
||||
<AreaChart
|
||||
h={300}
|
||||
data={chartData}
|
||||
dataKey="RequestCount"
|
||||
type="stacked"
|
||||
xAxisLabel="Data Count"
|
||||
yAxisLabel="CPU Usage (%)"
|
||||
withLegend
|
||||
withPointLabels={false}
|
||||
withDots={false}
|
||||
valueFormatter={(value) => new Intl.NumberFormat('en-GB').format(value)}
|
||||
areaChartProps={{ syncId: 'RequestCount' }}
|
||||
series={[
|
||||
{ name: 'gRpcServerDeserializeCPU', label: 'gRPC', color: 'teal.6' },
|
||||
{ name: 'RestServerDeserializeCPU', label: 'Rest', color: 'indigo.6' },
|
||||
]}
|
||||
/>
|
||||
<Divider my="0px" labelPosition="center" />
|
||||
</Grid.Col>
|
||||
}
|
||||
|
||||
{!hideServerSerialize &&
|
||||
<Grid.Col span={gridSpan} mb="md">
|
||||
<Text mb="md" pl="md" ta="center" fw={700}>Server Serialize CPU Usage (%)</Text>
|
||||
<AreaChart
|
||||
h={300}
|
||||
data={chartData}
|
||||
dataKey="RequestCount"
|
||||
type="stacked"
|
||||
xAxisLabel="Data Count"
|
||||
yAxisLabel="CPU Usage (%)"
|
||||
withLegend
|
||||
withPointLabels={false}
|
||||
withDots={false}
|
||||
valueFormatter={(value) => new Intl.NumberFormat('en-GB').format(value)}
|
||||
areaChartProps={{ syncId: 'RequestCount' }}
|
||||
series={[
|
||||
{ name: 'gRpcServerSerializeCPU', label: 'gRPC', color: 'teal.6' },
|
||||
{ name: 'RestServerSerializeCPU', label: 'Rest', color: 'indigo.6' },
|
||||
]}
|
||||
/>
|
||||
<Divider my="0px" labelPosition="center" />
|
||||
</Grid.Col>
|
||||
}
|
||||
|
||||
{!hideServerDeserialize &&
|
||||
<Grid.Col span={gridSpan} mb="md">
|
||||
<Text mb="md" pl="md" ta="center" fw={700}>Server Deserialize Time (ms)</Text>
|
||||
<AreaChart
|
||||
h={300}
|
||||
data={chartData}
|
||||
dataKey="RequestCount"
|
||||
type="stacked"
|
||||
xAxisLabel="Data Count"
|
||||
yAxisLabel="Time (ms)"
|
||||
withLegend
|
||||
withPointLabels={false}
|
||||
withDots={false}
|
||||
valueFormatter={(value) => new Intl.NumberFormat('en-GB').format(value * 1000)}
|
||||
areaChartProps={{ syncId: 'RequestCount' }}
|
||||
series={[
|
||||
{ name: 'gRpcServerDeserializeTime', label: 'gRPC', color: 'teal.6' },
|
||||
{ name: 'RestServerDeserializeTime', label: 'Rest', color: 'indigo.6' },
|
||||
]}
|
||||
/>
|
||||
<Divider my="0px" labelPosition="center" />
|
||||
</Grid.Col>
|
||||
}
|
||||
|
||||
{!hideServerSerialize &&
|
||||
<Grid.Col span={gridSpan} mb="md">
|
||||
<Text mb="md" pl="md" ta="center" fw={700}>Server Serialize Time (ms)</Text>
|
||||
<AreaChart
|
||||
h={300}
|
||||
data={chartData}
|
||||
dataKey="RequestCount"
|
||||
type="stacked"
|
||||
xAxisLabel="Data Count"
|
||||
yAxisLabel="Time (ms)"
|
||||
withLegend
|
||||
withPointLabels={false}
|
||||
withDots={false}
|
||||
valueFormatter={(value) => new Intl.NumberFormat('en-GB').format(value * 1000)}
|
||||
areaChartProps={{ syncId: 'RequestCount' }}
|
||||
series={[
|
||||
{ name: 'gRpcServerSerializeTime', label: 'gRPC', color: 'teal.6' },
|
||||
{ name: 'RestServerSerializeTime', label: 'Rest', color: 'indigo.6' },
|
||||
]}
|
||||
/>
|
||||
<Divider my="0px" labelPosition="center" />
|
||||
</Grid.Col>
|
||||
}
|
||||
|
||||
<Grid.Col span={gridSpan} mb="md">
|
||||
<Text mb="md" pl="md" ta="center" fw={700}>Server Protocol Total Time (ms)</Text>
|
||||
<AreaChart
|
||||
h={300}
|
||||
data={chartData}
|
||||
dataKey="RequestCount"
|
||||
type="stacked"
|
||||
xAxisLabel="Data Count"
|
||||
yAxisLabel="Time (ms)"
|
||||
withLegend
|
||||
withPointLabels={false}
|
||||
withDots={false}
|
||||
valueFormatter={(value) => new Intl.NumberFormat('en-GB').format(value * 1000)}
|
||||
areaChartProps={{ syncId: 'RequestCount' }}
|
||||
series={[
|
||||
{ name: 'gRpcServerProtocolTotalTime', label: 'gRPC', color: 'teal.6' },
|
||||
{ name: 'RestServerProtocolTotalTime', label: 'Rest', color: 'indigo.6' },
|
||||
]}
|
||||
/>
|
||||
<Divider my="0px" labelPosition="center" />
|
||||
</Grid.Col>
|
||||
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
47
rest_grpc_compare_frontend/app/components/GlobalAffix.tsx
Normal file
47
rest_grpc_compare_frontend/app/components/GlobalAffix.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import {Affix, Button, Group, rem, Stack} from "@mantine/core";
|
||||
import ArrowUpIcon from "mdi-react/ArrowUpIcon";
|
||||
import HomeIcon from "mdi-react/HomeIcon";
|
||||
import {useNavigate} from "react-router";
|
||||
import {useWindowScroll} from "@mantine/hooks";
|
||||
import {ThemeToggle} from "~/components/subs/ThemeToggle.tsx";
|
||||
import {roundThemeButton} from "~/styles.ts";
|
||||
|
||||
export default function () {
|
||||
const navigate = useNavigate();
|
||||
const [scroll, scrollTo] = useWindowScroll();
|
||||
|
||||
const buttonSize = 23;
|
||||
const circleSize = 55;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Affix position={{ bottom: 20, right: 20 }}>
|
||||
<Group dir="reverse">
|
||||
<Button
|
||||
radius={circleSize}
|
||||
w={circleSize}
|
||||
h={circleSize}
|
||||
p={0}
|
||||
disabled={scroll.y <= 0}
|
||||
onClick={() => scrollTo({ y: 0 })}
|
||||
>
|
||||
<ArrowUpIcon size={buttonSize} />
|
||||
</Button>
|
||||
|
||||
<ThemeToggle extraStyle={roundThemeButton} iconStyle={{width: rem(buttonSize - 3), height: rem(buttonSize - 3)}}/>
|
||||
|
||||
<Button
|
||||
radius={circleSize}
|
||||
w={circleSize}
|
||||
h={circleSize}
|
||||
p={0}
|
||||
onClick={() => navigate("/")}
|
||||
>
|
||||
<HomeIcon size={buttonSize}/>
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
</Affix>
|
||||
</>
|
||||
)
|
||||
}
|
||||
62
rest_grpc_compare_frontend/app/components/PageHeader.tsx
Normal file
62
rest_grpc_compare_frontend/app/components/PageHeader.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import {Burger, Group, Image, Text} from "@mantine/core";
|
||||
import {ThemeToggle} from "./subs/ThemeToggle.tsx";
|
||||
import React, {useEffect, useState} from "react";
|
||||
|
||||
|
||||
export function PageHeader({opened, toggle, appendTitle}: {opened: boolean, toggle: () => void, appendTitle?: string}) {
|
||||
const [width, setWidth] = useState(window.innerWidth)
|
||||
// const [height, setHeight] = useState(window.innerHeight)
|
||||
const [title, setTitle] = useState('Performance Test Dashboard');
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setWidth(window.innerWidth);
|
||||
// setHeight(window.innerHeight);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// useEffect(() => {
|
||||
// const titleElement = document.querySelector('title');
|
||||
// const observer = new MutationObserver((mutations) => {
|
||||
// mutations.forEach((mutation) => {
|
||||
// mutation.target.textContent && setTitle(mutation.target.textContent);
|
||||
// });
|
||||
// });
|
||||
//
|
||||
// if (titleElement) {
|
||||
// observer.observe(titleElement, { childList: true });
|
||||
// }
|
||||
//
|
||||
// return () => {
|
||||
// observer.disconnect();
|
||||
// };
|
||||
// }, []);
|
||||
|
||||
|
||||
return (
|
||||
<Group justify="space-between">
|
||||
<Group>
|
||||
<Burger
|
||||
opened={opened}
|
||||
onClick={toggle}
|
||||
hiddenFrom="sm"
|
||||
size="sm"
|
||||
/>
|
||||
<Image src="/icon.png" w={30} h={30}/>
|
||||
{width >= 290 && <Text size="lg" fw={700}>{title}</Text>}
|
||||
{appendTitle && <Text size="sm">{appendTitle}</Text>}
|
||||
</Group>
|
||||
|
||||
<Group>
|
||||
<ThemeToggle/>
|
||||
</Group>
|
||||
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
56
rest_grpc_compare_frontend/app/components/PageNavbar.tsx
Normal file
56
rest_grpc_compare_frontend/app/components/PageNavbar.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import {Button, Divider, Flex, Tabs} from "@mantine/core";
|
||||
import {iconMStyle, maxWidth} from "../styles.ts";
|
||||
import React from "react";
|
||||
import {DashboardPageType} from "~/utils/enums.ts";
|
||||
import HomeIcon from "mdi-react/HomeIcon";
|
||||
import InformationIcon from "mdi-react/InformationIcon";
|
||||
import LogoutIcon from "mdi-react/LogoutIcon";
|
||||
import DownloadIcon from "mdi-react/DownloadIcon";
|
||||
import UploadIcon from "mdi-react/UploadIcon";
|
||||
|
||||
|
||||
export function PageNavbar({currentStat, changePageStat}:
|
||||
{currentStat: DashboardPageType, changePageStat: (p: DashboardPageType) => any}) {
|
||||
|
||||
const onClickLogout = () => {
|
||||
localStorage.removeItem("password")
|
||||
changePageStat(DashboardPageType.Home)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs variant="pills" orientation="vertical" defaultValue="gallery" value={currentStat} style={{flex: 1}}
|
||||
onChange={(e) => {if (e) changePageStat(e as DashboardPageType)}}>
|
||||
<Tabs.List style={maxWidth}>
|
||||
<Tabs.Tab value={DashboardPageType.Dashboard} leftSection={<HomeIcon style={iconMStyle}/>}>
|
||||
Home
|
||||
</Tabs.Tab>
|
||||
|
||||
<Divider my="0px" label="Performance Test" labelPosition="center" />
|
||||
|
||||
<Tabs.Tab value={DashboardPageType.DashboardTestGetList} leftSection={<DownloadIcon style={iconMStyle}/>}>
|
||||
Fetch Data
|
||||
</Tabs.Tab>
|
||||
|
||||
<Tabs.Tab value={DashboardPageType.DashboardTestPushData} leftSection={<UploadIcon style={iconMStyle}/>}>
|
||||
Push Data
|
||||
</Tabs.Tab>
|
||||
|
||||
</Tabs.List>
|
||||
</Tabs>
|
||||
|
||||
<Flex gap="md" justify="flex-start" align="flex-end" direction="row" wrap="wrap" style={maxWidth}>
|
||||
|
||||
<Button variant="outline" color="blue" fullWidth justify="start" onClick={() => changePageStat(DashboardPageType.About)}
|
||||
leftSection={<InformationIcon style={iconMStyle}/>}>
|
||||
About
|
||||
</Button>
|
||||
<Button variant="outline" color="red" fullWidth justify="start" onClick={onClickLogout}
|
||||
leftSection={<LogoutIcon style={iconMStyle}/>}>
|
||||
Logout
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
275
rest_grpc_compare_frontend/app/components/PerformanceTest.tsx
Normal file
275
rest_grpc_compare_frontend/app/components/PerformanceTest.tsx
Normal file
@ -0,0 +1,275 @@
|
||||
import {
|
||||
Stack,
|
||||
Group,
|
||||
Card,
|
||||
Accordion,
|
||||
Text,
|
||||
Table,
|
||||
RingProgress,
|
||||
ScrollArea,
|
||||
Space
|
||||
} from "@mantine/core";
|
||||
import {marginTopBottom} from "~/styles.ts";
|
||||
import {PerformanceTestFilter} from "~/components/subs/PerformanceTestFilter.tsx";
|
||||
import { ResponsiveTableContainer } from "~/components/subs/ResponsiveTableContainer";
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import type {BookInfo, PerformanceGetListData} from "~/utils/models.ts";
|
||||
import { proxy, useSnapshot } from 'valtio';
|
||||
import {convertNumber, showErrorMessage, sleep} from "~/utils/utils.ts";
|
||||
import {apiPerformanceGetList, apiPerformanceUploadData} from "~/utils/compare_api.ts";
|
||||
import Charts from "~/components/Charts.tsx";
|
||||
import {getTestExampleData, pushTestExampleData} from "~/exapleData.ts";
|
||||
import {BookTable} from "~/components/subs/BookInfoTable.tsx";
|
||||
import {confirmExportData} from "~/components/subs/confirms.tsx";
|
||||
|
||||
// const performanceDataState = proxy<PerformanceGetListData[]>([]);
|
||||
|
||||
export default function Component({isPushTest, performanceDataState}: { isPushTest: boolean, performanceDataState: PerformanceGetListData[] }) {
|
||||
const [startsCount, setStartsCount] = useState(200)
|
||||
const [endsCount, setEndsCount] = useState(100000)
|
||||
const [addCount, setAddCount] = useState(200)
|
||||
const [avgCount, setAvgCount] = useState(3)
|
||||
const [maxExampleCount, setMaxExampleCount] = useState(30)
|
||||
|
||||
const [isStarting, setIsStarting] = useState(false)
|
||||
const [requestStopping, setRequestStopping] = useState(false)
|
||||
const requestStoppingRef = useRef(requestStopping);
|
||||
requestStoppingRef.current = requestStopping;
|
||||
|
||||
const [currentCount, setCurrentCount] = useState(0)
|
||||
const [currentIdx, setCurrentIdx] = useState(0)
|
||||
const [currentAvgCount, setCurrentAvgCount] = useState(0)
|
||||
|
||||
const [hideServerDeserialize, setHideServerDeserialize] = useState(!isPushTest)
|
||||
const [hideServerSerialize, setHideServerSerialize] = useState(isPushTest)
|
||||
const [chartsSize, setChartsSize] = useState(6)
|
||||
|
||||
const performanceDataSnap = useSnapshot(performanceDataState);
|
||||
|
||||
useEffect(() => {
|
||||
getExampleData()
|
||||
}, [])
|
||||
|
||||
const exportData = () => {
|
||||
if (!performanceDataSnap) return
|
||||
confirmExportData(performanceDataSnap as PerformanceGetListData[], isPushTest ? "performance_test_push_data" : "performance_test_get_list_data")
|
||||
}
|
||||
|
||||
const updateFilter = (startsCount: number, endsCount: number, addCount: number, avgCount: number, maxExampleCount: number, hideServerDeserialize: boolean, hideServerSerialize: boolean, chartsSize: number) => {
|
||||
setStartsCount(startsCount);
|
||||
setEndsCount(endsCount);
|
||||
setAddCount(addCount);
|
||||
setAvgCount(avgCount);
|
||||
setMaxExampleCount(maxExampleCount);
|
||||
setHideServerDeserialize(hideServerDeserialize);
|
||||
setHideServerSerialize(hideServerSerialize);
|
||||
setChartsSize(chartsSize);
|
||||
}
|
||||
|
||||
const onClickStart = () => {
|
||||
StartTest()
|
||||
.finally(() => setIsStarting(false))
|
||||
}
|
||||
|
||||
const OnClickStop = () => {
|
||||
if (!isStarting) return
|
||||
setRequestStopping(true)
|
||||
}
|
||||
|
||||
const getCurrentSingleStep = (queryIdx: number | undefined) => {
|
||||
if (!isStarting) return queryIdx;
|
||||
if (queryIdx !== currentIdx) return queryIdx;
|
||||
let value = currentAvgCount / avgCount * 100;
|
||||
return <RingProgress
|
||||
sections={[{ value, color: 'blue' }]}
|
||||
transitionDuration={250}
|
||||
label={<Text ta="center">{currentAvgCount} / {avgCount}</Text>}
|
||||
/>
|
||||
|
||||
}
|
||||
|
||||
const getExampleData = () => {
|
||||
performanceDataState.splice(0);
|
||||
let exampleData = isPushTest ? pushTestExampleData : getTestExampleData
|
||||
for (let i = 0; i < exampleData.length; i++) {
|
||||
// @ts-ignore
|
||||
performanceDataState.push(exampleData[i])
|
||||
}
|
||||
}
|
||||
|
||||
const StartTest = async () => {
|
||||
setIsStarting(true)
|
||||
setRequestStopping(false)
|
||||
|
||||
if (addCount <= 0) {
|
||||
showErrorMessage("Step Count must be greater than 0", "Invalid Step Count");
|
||||
return;
|
||||
}
|
||||
|
||||
if (startsCount > endsCount) {
|
||||
showErrorMessage("Starts Count can not be greater than Ends Count", "Invalid Starts Count");
|
||||
return;
|
||||
}
|
||||
|
||||
let currentForCount = startsCount;
|
||||
let idx = 0;
|
||||
performanceDataState.splice(0);
|
||||
|
||||
while (true) {
|
||||
setCurrentCount(currentForCount);
|
||||
setCurrentIdx(idx)
|
||||
|
||||
for (let i = 0; i < avgCount; i++) {
|
||||
try {
|
||||
if (requestStoppingRef.current) {
|
||||
setRequestStopping(false)
|
||||
return
|
||||
}
|
||||
|
||||
let response = isPushTest ?
|
||||
await apiPerformanceUploadData(currentForCount, idx) :
|
||||
await apiPerformanceGetList(currentForCount, maxExampleCount, idx)
|
||||
|
||||
if (response.success && response.data) {
|
||||
const checkLastIndex = performanceDataState.length - 1
|
||||
if (performanceDataState.length > 0 && performanceDataState[checkLastIndex].idx === response.data.idx) {
|
||||
const lastData = performanceDataState[checkLastIndex]
|
||||
performanceDataState[checkLastIndex].grpc.client_networking_ping = (lastData.grpc.client_networking_ping + response.data.grpc.client_networking_ping) / 2;
|
||||
performanceDataState[checkLastIndex].grpc.client_request_cpu = (lastData.grpc.client_request_cpu + response.data.grpc.client_request_cpu) / 2;
|
||||
performanceDataState[checkLastIndex].grpc.client_request_time = (lastData.grpc.client_request_time + response.data.grpc.client_request_time) / 2;
|
||||
performanceDataState[checkLastIndex].grpc.networking_size.request_size = (lastData.grpc.networking_size.request_size + response.data.grpc.networking_size.request_size) / 2;
|
||||
performanceDataState[checkLastIndex].grpc.networking_size.response_size = (lastData.grpc.networking_size.response_size + response.data.grpc.networking_size.response_size) / 2;
|
||||
performanceDataState[checkLastIndex].grpc.server_deserialize_cpu = (lastData.grpc.server_deserialize_cpu + response.data.grpc.server_deserialize_cpu) / 2;
|
||||
performanceDataState[checkLastIndex].grpc.server_deserialize_time = (lastData.grpc.server_deserialize_time + response.data.grpc.server_deserialize_time) / 2;
|
||||
performanceDataState[checkLastIndex].grpc.server_serialize_cpu = (lastData.grpc.server_serialize_cpu + response.data.grpc.server_serialize_cpu) / 2;
|
||||
performanceDataState[checkLastIndex].grpc.server_serialize_time = (lastData.grpc.server_serialize_time + response.data.grpc.server_serialize_time) / 2;
|
||||
performanceDataState[checkLastIndex].grpc.server_protocol_total_time = (lastData.grpc.server_protocol_total_time + response.data.grpc.server_protocol_total_time) / 2;
|
||||
|
||||
performanceDataState[checkLastIndex].rest.client_networking_ping = (lastData.rest.client_networking_ping + response.data.rest.client_networking_ping) / 2;
|
||||
performanceDataState[checkLastIndex].rest.client_request_cpu = (lastData.rest.client_request_cpu + response.data.rest.client_request_cpu) / 2;
|
||||
performanceDataState[checkLastIndex].rest.client_request_time = (lastData.rest.client_request_time + response.data.rest.client_request_time) / 2;
|
||||
performanceDataState[checkLastIndex].rest.networking_size.request_size = (lastData.rest.networking_size.request_size + response.data.rest.networking_size.request_size) / 2;
|
||||
performanceDataState[checkLastIndex].rest.networking_size.response_size = (lastData.rest.networking_size.response_size + response.data.rest.networking_size.response_size) / 2;
|
||||
performanceDataState[checkLastIndex].rest.server_deserialize_cpu = (lastData.rest.server_deserialize_cpu + response.data.rest.server_deserialize_cpu) / 2;
|
||||
performanceDataState[checkLastIndex].rest.server_deserialize_time = (lastData.rest.server_deserialize_time + response.data.rest.server_deserialize_time) / 2;
|
||||
performanceDataState[checkLastIndex].rest.server_serialize_cpu = (lastData.rest.server_serialize_cpu + response.data.rest.server_serialize_cpu) / 2;
|
||||
performanceDataState[checkLastIndex].rest.server_serialize_time = (lastData.rest.server_serialize_time + response.data.rest.server_serialize_time) / 2;
|
||||
performanceDataState[checkLastIndex].rest.server_protocol_total_time = (lastData.rest.server_protocol_total_time + response.data.rest.server_protocol_total_time) / 2;
|
||||
}
|
||||
else {
|
||||
performanceDataState.push(response.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error: any) {
|
||||
showErrorMessage(error.toString(), "Error during performance test");
|
||||
}
|
||||
finally {
|
||||
setCurrentAvgCount(i + 1)
|
||||
}
|
||||
}
|
||||
idx++;
|
||||
setCurrentIdx(idx)
|
||||
await sleep(200)
|
||||
|
||||
if (currentForCount >= endsCount) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentForCount += addCount;
|
||||
if (currentForCount > endsCount) {
|
||||
currentForCount = endsCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rows = performanceDataSnap.map(item => (
|
||||
<Table.Tr key={`${item.idx}_${item.request_count}`}>
|
||||
<Table.Td>{getCurrentSingleStep(item.idx)}</Table.Td>
|
||||
<Table.Td>{item.request_count}</Table.Td>
|
||||
<Table.Td>{convertNumber(item.grpc.client_networking_ping, 1000, 2)} / {convertNumber(item.rest.client_networking_ping, 1000, 2)}</Table.Td>
|
||||
<Table.Td>{convertNumber(item.grpc.client_request_cpu, 1, 2)} / {convertNumber(item.rest.client_request_cpu, 1, 2)}</Table.Td>
|
||||
<Table.Td>{convertNumber(item.grpc.client_request_time, 1, 2)} / {convertNumber(item.rest.client_request_time, 1, 2)}</Table.Td>
|
||||
<Table.Td>{convertNumber(item.grpc.server_deserialize_cpu, 1, 2)} / {convertNumber(item.rest.server_deserialize_cpu, 1, 2)}</Table.Td>
|
||||
<Table.Td>{convertNumber(item.grpc.server_deserialize_time, 1000, 4)} / {convertNumber(item.rest.server_deserialize_time, 1000, 4)}</Table.Td>
|
||||
<Table.Td>{convertNumber(item.grpc.server_serialize_cpu, 1, 2)} / {convertNumber(item.rest.server_serialize_cpu, 1, 2)}</Table.Td>
|
||||
<Table.Td>{convertNumber(item.grpc.server_serialize_time, 1000, 4)} / {convertNumber(item.rest.server_serialize_time, 1000, 4)}</Table.Td>
|
||||
<Table.Td>{convertNumber(item.grpc.networking_size.request_size, 1 / 1024, 4)} / {convertNumber(item.rest.networking_size.request_size, 1 / 1024, 4)}</Table.Td>
|
||||
<Table.Td>{convertNumber(item.grpc.networking_size.response_size, 1 / 1024, 4)} / {convertNumber(item.rest.networking_size.response_size, 1 / 1024, 4)}</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Card padding="lg" radius="md" withBorder style={marginTopBottom}>
|
||||
|
||||
<Card.Section withBorder>
|
||||
<Accordion variant="filled" chevronPosition="left" defaultValue="advanced-filter">
|
||||
<Accordion.Item value="advanced-filter">
|
||||
<Accordion.Control>
|
||||
<Group>
|
||||
<Text>Config</Text>
|
||||
</Group>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<PerformanceTestFilter onChange={updateFilter} onClickStart={onClickStart} onClickStop={OnClickStop} onClickExportData={exportData}
|
||||
isStarting={isStarting} currentCount={currentCount} requestStopping={requestStopping} isPushTest={isPushTest} />
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
</Card.Section>
|
||||
|
||||
<Card.Section withBorder>
|
||||
<Charts data={performanceDataSnap.flatMap(item => item || []) as PerformanceGetListData[]}
|
||||
gridSpan={chartsSize} hideServerDeserialize={hideServerDeserialize} hideServerSerialize={hideServerSerialize} />
|
||||
</Card.Section>
|
||||
|
||||
<Card.Section>
|
||||
<ResponsiveTableContainer minWidth={600}>
|
||||
<Table striped highlightOnHover withColumnBorders withTableBorder>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th colSpan={2}></Table.Th>
|
||||
<Table.Th colSpan={3}>Client (gRPC / Rest)</Table.Th>
|
||||
<Table.Th colSpan={2}>Server Deserialization (gRPC / Rest)</Table.Th>
|
||||
<Table.Th colSpan={2}>Server Serialization (gRPC / Rest)</Table.Th>
|
||||
<Table.Th colSpan={2}>Networking Pack Size (gRPC / Rest)</Table.Th>
|
||||
</Table.Tr>
|
||||
<Table.Tr key="header">
|
||||
<Table.Th>Index</Table.Th>
|
||||
<Table.Th>Count</Table.Th>
|
||||
<Table.Th>Ping (ms)</Table.Th>
|
||||
<Table.Th>CPU (%)</Table.Th>
|
||||
<Table.Th>Time (s)</Table.Th>
|
||||
<Table.Th>CPU (%)</Table.Th>
|
||||
<Table.Th>Time (ms)</Table.Th>
|
||||
<Table.Th>CPU (%)</Table.Th>
|
||||
<Table.Th>Time (ms)</Table.Th>
|
||||
<Table.Th>Request (KB)</Table.Th>
|
||||
<Table.Th>Response (KB)</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{rows}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</ResponsiveTableContainer>
|
||||
|
||||
{/*<Pagination withEdges total={staffInfo.total_pages} value={currPage} onChange={setCurrPage} mt="sm"*/}
|
||||
{/* style={{justifyItems: "flex-end", ...marginRightBottom}}/>*/}
|
||||
</Card.Section>
|
||||
|
||||
{!isPushTest &&
|
||||
<Card.Section>
|
||||
<Space h="lg"/>
|
||||
<Text ta="center" fw={700}>Example Books Data</Text>
|
||||
<ScrollArea w="100%">
|
||||
<BookTable books={(performanceDataSnap && performanceDataSnap.length > 0 ? performanceDataSnap[performanceDataSnap.length - 1].rest.response_data || [] : []) as BookInfo[]} />
|
||||
</ScrollArea>
|
||||
</Card.Section>
|
||||
}
|
||||
</Card>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,81 @@
|
||||
import { Table } from "@mantine/core";
|
||||
import type {BookInfo} from "~/utils/models.ts";
|
||||
|
||||
|
||||
export const BookTable = ({ books }: {books: BookInfo[]}) => {
|
||||
// 格式化发布日期
|
||||
const formatDate = (timestamp: string) => {
|
||||
const date = new Date(parseInt(timestamp) * 1000);
|
||||
return date.toLocaleDateString();
|
||||
};
|
||||
|
||||
// 生成表格行
|
||||
const rows = books.map((book, index) => (
|
||||
<Table.Tr key={book.isbn || index}>
|
||||
<Table.Td>{book.title}</Table.Td>
|
||||
<Table.Td>{book.subtitle}</Table.Td>
|
||||
<Table.Td>{book.author}</Table.Td>
|
||||
<Table.Td>{book.editor}</Table.Td>
|
||||
<Table.Td>{book.translator}</Table.Td>
|
||||
<Table.Td>{book.publisher}</Table.Td>
|
||||
<Table.Td>{formatDate(book.publication_date)}</Table.Td>
|
||||
<Table.Td>{book.isbn}</Table.Td>
|
||||
<Table.Td>{book.barcode}</Table.Td>
|
||||
<Table.Td>{book.language}</Table.Td>
|
||||
<Table.Td>{book.pages}</Table.Td>
|
||||
<Table.Td>{book.format}</Table.Td>
|
||||
<Table.Td>{book.binding}</Table.Td>
|
||||
<Table.Td>{book.edition}</Table.Td>
|
||||
<Table.Td>{book.weight.toFixed(2)} g</Table.Td>
|
||||
<Table.Td>{book.category_id}</Table.Td>
|
||||
<Table.Td>{book.subject}</Table.Td>
|
||||
<Table.Td style={{ maxWidth: '200px', wordBreak: 'break-word' }}>
|
||||
{book.keywords}
|
||||
</Table.Td>
|
||||
<Table.Td style={{ maxWidth: '300px', wordBreak: 'break-word' }}>
|
||||
{book.abstract}
|
||||
</Table.Td>
|
||||
<Table.Td style={{ maxWidth: '300px', wordBreak: 'break-word' }}>
|
||||
{book.description}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<a href={book.cover_image} target="_blank" rel="noopener noreferrer">
|
||||
View Image
|
||||
</a>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
));
|
||||
|
||||
return (
|
||||
<Table striped highlightOnHover withColumnBorders withTableBorder w="200%">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Title</Table.Th>
|
||||
<Table.Th>Subtitle</Table.Th>
|
||||
<Table.Th>Author</Table.Th>
|
||||
<Table.Th>Editor</Table.Th>
|
||||
<Table.Th>Translator</Table.Th>
|
||||
<Table.Th>Publisher</Table.Th>
|
||||
<Table.Th>Publication Date</Table.Th>
|
||||
<Table.Th>ISBN</Table.Th>
|
||||
<Table.Th>Barcode</Table.Th>
|
||||
<Table.Th>Language</Table.Th>
|
||||
<Table.Th>Pages</Table.Th>
|
||||
<Table.Th>Format</Table.Th>
|
||||
<Table.Th>Binding</Table.Th>
|
||||
<Table.Th>Edition</Table.Th>
|
||||
<Table.Th>Weight</Table.Th>
|
||||
<Table.Th>Category ID</Table.Th>
|
||||
<Table.Th>Subject</Table.Th>
|
||||
<Table.Th>Keywords</Table.Th>
|
||||
<Table.Th>Abstract</Table.Th>
|
||||
<Table.Th>Description</Table.Th>
|
||||
<Table.Th>Cover Image</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{rows}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,152 @@
|
||||
import {
|
||||
Button, Checkbox, Flex,
|
||||
Grid, Group,
|
||||
NumberInput,
|
||||
Progress, Slider, Stack, Text,
|
||||
} from "@mantine/core";
|
||||
import React, {useEffect, useMemo, useState} from "react";
|
||||
import {flexLeftWeight, maxWidth} from "~/styles.ts";
|
||||
import DownloadIcon from "mdi-react/DownloadIcon";
|
||||
import PlayIcon from "mdi-react/PlayIcon";
|
||||
import StopIcon from "mdi-react/StopIcon";
|
||||
|
||||
|
||||
export function PerformanceTestFilter(
|
||||
{onChange, onClickStart, onClickStop, onClickExportData, isStarting, currentCount, requestStopping, isPushTest}: {
|
||||
onChange: (startsCount: number, endsCount: number, addCount: number, avgCount: number, maxExampleCount: number, hideServerDeserialize: boolean,
|
||||
hideServerSerialize: boolean, chartsSize: number) => void,
|
||||
onClickStart: () => void,
|
||||
onClickStop: () => void,
|
||||
onClickExportData: () => void,
|
||||
isStarting: boolean,
|
||||
requestStopping: boolean,
|
||||
currentCount: number,
|
||||
isPushTest: boolean
|
||||
}
|
||||
) {
|
||||
const [startsCount, setStartsCount] = useState(200)
|
||||
const [endsCount, setEndsCount] = useState(100000)
|
||||
const [addCount, setAddCount] = useState(200)
|
||||
const [avgCount, setAvgCount] = useState(3)
|
||||
const [maxExampleCount, setMaxExampleCount] = useState(30)
|
||||
const [hideServerDeserialize, setHideServerDeserialize] = useState(!isPushTest)
|
||||
const [hideServerSerialize, setHideServerSerialize] = useState(isPushTest)
|
||||
const [chartsSize, setChartsSize] = useState(6)
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
onChange(startsCount, endsCount, addCount, avgCount, maxExampleCount, hideServerDeserialize, hideServerSerialize, chartsSize)
|
||||
}, [startsCount, endsCount, addCount, avgCount, maxExampleCount, hideServerDeserialize, hideServerSerialize, chartsSize]);
|
||||
|
||||
const getCurrentProgress = (start: number, end: number, value: number) => {
|
||||
if (end <= start) return 0;
|
||||
return Math.min(Math.max(0, value - start), end - start) / (end - start) * 100;
|
||||
}
|
||||
|
||||
const validChartsSizeValues = [1, 2, 3, 4, 6, 12];
|
||||
|
||||
return (
|
||||
<Grid gutter="xs" grow>
|
||||
<Grid.Col span={4} mb="md">
|
||||
<NumberInput
|
||||
label="Starts Count"
|
||||
description="Starting number of records"
|
||||
thousandSeparator=","
|
||||
value={startsCount}
|
||||
disabled={isStarting}
|
||||
onChange={(value) => (setStartsCount(typeof value === 'number' ? value : parseInt(value)))}
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={4} mb="md">
|
||||
<NumberInput
|
||||
label="Ends Count"
|
||||
description="Maximum number of records"
|
||||
max={110000}
|
||||
thousandSeparator=","
|
||||
value={endsCount}
|
||||
disabled={isStarting}
|
||||
onChange={(value) => (setEndsCount(typeof value === 'number' ? value : parseInt(value)))}
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={4} mb="md">
|
||||
<NumberInput
|
||||
label="Step Count"
|
||||
description="Records to add per test iteration"
|
||||
thousandSeparator=","
|
||||
value={addCount}
|
||||
disabled={isStarting}
|
||||
onChange={(value) => (setAddCount(typeof value === 'number' ? value : parseInt(value)))}
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6} mb="md">
|
||||
<NumberInput
|
||||
label="Iterations"
|
||||
description="Number of times to repeat each test"
|
||||
thousandSeparator=","
|
||||
value={avgCount}
|
||||
disabled={isStarting}
|
||||
onChange={(value) => (setAvgCount(typeof value === 'number' ? value : parseInt(value)))}
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
{!isPushTest &&
|
||||
<Grid.Col span={6} mb="md">
|
||||
<NumberInput
|
||||
label="Sample Limit"
|
||||
description="Max records to display in preview"
|
||||
thousandSeparator=","
|
||||
value={maxExampleCount}
|
||||
disabled={isStarting}
|
||||
onChange={(value) => (setMaxExampleCount(typeof value === 'number' ? value : parseInt(value)))}
|
||||
/>
|
||||
</Grid.Col>
|
||||
}
|
||||
|
||||
<Grid.Col span={7} mb="md">
|
||||
<Text fw={500} size="sm">Charts Size</Text>
|
||||
<Slider min={1} max={6} defaultValue={5}
|
||||
marks={Array.from({ length: 6 }, (_, i) => ({
|
||||
value: i + 1,
|
||||
label: `${i + 1}`
|
||||
}))}
|
||||
value={validChartsSizeValues.indexOf(chartsSize) + 1} onChange={(value) => setChartsSize(validChartsSizeValues[value - 1])} />
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={3} mb="md">
|
||||
<Stack h="95%" justify="flex-end">
|
||||
<Group>
|
||||
<Checkbox label="Hide Server Deserialize Info Chart" checked={hideServerDeserialize} defaultChecked={hideServerDeserialize}
|
||||
onChange={(event) => setHideServerDeserialize(event.currentTarget.checked)}/>
|
||||
<Checkbox label="Hide Server Serialize Info Chart" checked={hideServerSerialize} defaultChecked={hideServerSerialize}
|
||||
onChange={(event) => setHideServerSerialize(event.currentTarget.checked)}/>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={12} mb="md">
|
||||
<Flex align="center" gap="lg">
|
||||
<Stack gap="1px" style={flexLeftWeight}>
|
||||
<Text size="sm">{`${currentCount} / ${endsCount}`}</Text>
|
||||
<Progress.Root size="xl">
|
||||
<Progress.Section value={getCurrentProgress(startsCount, endsCount, currentCount)} animated={isStarting}>
|
||||
<Progress.Label>{`${currentCount} / ${endsCount}`}</Progress.Label>
|
||||
</Progress.Section>
|
||||
</Progress.Root>
|
||||
</Stack>
|
||||
|
||||
<Group>
|
||||
{
|
||||
isStarting
|
||||
? <Button color="red" onClick={onClickStop} disabled={requestStopping} leftSection={<StopIcon />}>Stop</Button>
|
||||
: <Button onClick={onClickStart} leftSection={<PlayIcon />}>Start</Button>
|
||||
}
|
||||
<Button leftSection={<DownloadIcon />} onClick={onClickExportData}>Export</Button>
|
||||
</Group>
|
||||
</Flex>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
import { type ReactNode, forwardRef } from 'react';
|
||||
import { Table } from '@mantine/core';
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
|
||||
type ResponsiveTableContainerProps = Omit<
|
||||
React.ComponentProps<typeof Table.ScrollContainer>,
|
||||
'children'
|
||||
> & {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const ResponsiveTableContainer = forwardRef<
|
||||
HTMLDivElement,
|
||||
ResponsiveTableContainerProps
|
||||
>(({ children, ...rest }, ref) => {
|
||||
const isNarrow = useMediaQuery('(max-width: 700px)');
|
||||
|
||||
return isNarrow ? (
|
||||
<Table.ScrollContainer ref={ref} {...rest}>
|
||||
{children}
|
||||
</Table.ScrollContainer>
|
||||
) : (
|
||||
<>{children}</>
|
||||
);
|
||||
});
|
||||
|
||||
ResponsiveTableContainer.displayName = 'ResponsiveTableContainer';
|
||||
@ -0,0 +1,41 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Group, rem,
|
||||
Tooltip,
|
||||
useComputedColorScheme,
|
||||
useMantineColorScheme,
|
||||
useMantineTheme
|
||||
} from "@mantine/core";
|
||||
import {iconMStyle} from "~/styles";
|
||||
|
||||
import ThemeLightDarkIcon from "mdi-react/ThemeLightDarkIcon";
|
||||
import WeatherNightIcon from "mdi-react/WeatherNightIcon";
|
||||
import WeatherSunnyIcon from "mdi-react/WeatherSunnyIcon";
|
||||
import type {CSSProperties} from "react";
|
||||
|
||||
|
||||
export function ThemeToggle({extraStyle, iconStyle}: {extraStyle?: CSSProperties, iconStyle?: CSSProperties}) {
|
||||
const { colorScheme, setColorScheme } = useMantineColorScheme();
|
||||
// const [colorSchemeState, toggleColorSchemeState] = useToggle(['auto', 'dark', 'light'] as const);
|
||||
const computedColorScheme = useComputedColorScheme('light');
|
||||
const theme = useMantineTheme();
|
||||
|
||||
const nextScheme = colorScheme === "auto" ? "dark" : colorScheme === "dark" ? "light" : "auto";
|
||||
|
||||
const onClickChangeColorScheme = () => {
|
||||
setColorScheme(nextScheme)
|
||||
}
|
||||
|
||||
return (
|
||||
<Group justify="center">
|
||||
<Tooltip label={colorScheme === 'auto' ? 'Auto' : colorScheme === 'dark' ? 'Dark' : 'Light'} position="left">
|
||||
<ActionIcon variant="light" size="md" onClick={onClickChangeColorScheme} color={
|
||||
colorScheme === 'auto' ? undefined : computedColorScheme === 'dark' ? theme.colors.blue[4] : theme.colors.yellow[6]
|
||||
} style={ extraStyle ? {...extraStyle} : {} }>
|
||||
{colorScheme === 'auto' ? <ThemeLightDarkIcon style={iconStyle || iconMStyle}/> :
|
||||
colorScheme === 'dark' ? <WeatherNightIcon style={iconStyle || iconMStyle}/> : <WeatherSunnyIcon style={iconStyle || iconMStyle}/>}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
152
rest_grpc_compare_frontend/app/components/subs/confirms.tsx
Normal file
152
rest_grpc_compare_frontend/app/components/subs/confirms.tsx
Normal file
@ -0,0 +1,152 @@
|
||||
import {modals} from "@mantine/modals";
|
||||
import {Button, Group, Stack, Text} from "@mantine/core";
|
||||
import {showInfoMessage} from "~/utils/utils.ts";
|
||||
import type {PerformanceGetListData} from "~/utils/models.ts";
|
||||
|
||||
export function confirmExportData(exportObject: PerformanceGetListData[], fileName: string) {
|
||||
const onClickExportJson = (download: boolean) => {
|
||||
const dataStr = JSON.stringify(exportObject, null, 4)
|
||||
|
||||
const blob = new Blob([dataStr], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
if (download) {
|
||||
a.download = `${fileName}_${Date.now()}.json`
|
||||
}
|
||||
else {
|
||||
a.target = '_blank'
|
||||
}
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
showInfoMessage("Data exported successfully", "Export Data")
|
||||
|
||||
modals.closeAll()
|
||||
}
|
||||
|
||||
const onClickExportCsv = () => {
|
||||
const csvData = exportObject.map(item => {
|
||||
// 处理 grpc 数据,去掉 response_data,展开 networking_size
|
||||
const grpcData = { ...item.grpc };
|
||||
delete grpcData.response_data;
|
||||
const grpcFlattened = {
|
||||
...grpcData,
|
||||
networking_size_request_size: grpcData.networking_size?.request_size || 0,
|
||||
networking_size_response_size: grpcData.networking_size?.response_size || 0
|
||||
};
|
||||
// @ts-ignore
|
||||
delete grpcFlattened.networking_size;
|
||||
|
||||
|
||||
const restData = { ...item.rest };
|
||||
delete restData.response_data;
|
||||
const restFlattened = {
|
||||
...restData,
|
||||
networking_size_request_size: restData.networking_size?.request_size || 0,
|
||||
networking_size_response_size: restData.networking_size?.response_size || 0
|
||||
};
|
||||
// @ts-ignore
|
||||
delete restFlattened.networking_size;
|
||||
|
||||
// 组合最终的行数据
|
||||
return {
|
||||
idx: item.idx,
|
||||
request_count: item.request_count,
|
||||
grpc_client_networking_ping: grpcFlattened.client_networking_ping,
|
||||
grpc_client_request_cpu: grpcFlattened.client_request_cpu,
|
||||
grpc_client_request_time: grpcFlattened.client_request_time,
|
||||
grpc_server_deserialize_cpu: grpcFlattened.server_deserialize_cpu,
|
||||
grpc_server_deserialize_time: grpcFlattened.server_deserialize_time,
|
||||
grpc_server_serialize_cpu: grpcFlattened.server_serialize_cpu,
|
||||
grpc_server_serialize_time: grpcFlattened.server_serialize_time,
|
||||
grpc_server_protocol_total_time: grpcFlattened.server_protocol_total_time,
|
||||
grpc_networking_size_request_size: grpcFlattened.networking_size_request_size,
|
||||
grpc_networking_size_response_size: grpcFlattened.networking_size_response_size,
|
||||
|
||||
rest_client_networking_ping: restFlattened.client_networking_ping,
|
||||
rest_client_request_cpu: restFlattened.client_request_cpu,
|
||||
rest_client_request_time: restFlattened.client_request_time,
|
||||
rest_server_deserialize_cpu: restFlattened.server_deserialize_cpu,
|
||||
rest_server_deserialize_time: restFlattened.server_deserialize_time,
|
||||
rest_server_serialize_cpu: restFlattened.server_serialize_cpu,
|
||||
rest_server_serialize_time: restFlattened.server_serialize_time,
|
||||
rest_server_protocol_total_time: restFlattened.server_protocol_total_time,
|
||||
rest_networking_size_request_size: restFlattened.networking_size_request_size,
|
||||
rest_networking_size_response_size: restFlattened.networking_size_response_size,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
const headers = [
|
||||
'idx',
|
||||
'request_count',
|
||||
'grpc_client_networking_ping',
|
||||
'grpc_client_request_cpu',
|
||||
'grpc_client_request_time',
|
||||
'grpc_server_deserialize_cpu',
|
||||
'grpc_server_deserialize_time',
|
||||
'grpc_server_serialize_cpu',
|
||||
'grpc_server_serialize_time',
|
||||
'grpc_server_protocol_total_time',
|
||||
'grpc_networking_size_request_size',
|
||||
'grpc_networking_size_response_size',
|
||||
'rest_client_networking_ping',
|
||||
'rest_client_request_cpu',
|
||||
'rest_client_request_time',
|
||||
'rest_server_deserialize_cpu',
|
||||
'rest_server_deserialize_time',
|
||||
'rest_server_serialize_cpu',
|
||||
'rest_server_serialize_time',
|
||||
'rest_server_protocol_total_time',
|
||||
'rest_networking_size_request_size',
|
||||
'rest_networking_size_response_size'
|
||||
];
|
||||
|
||||
// 生成 CSV 内容
|
||||
const csvContent = [
|
||||
headers.join(','),
|
||||
...csvData.map(row =>
|
||||
headers.map(header => {
|
||||
return row[header as keyof typeof row];
|
||||
}).join(',')
|
||||
)
|
||||
].join('\n');
|
||||
|
||||
const BOM = '\uFEFF';
|
||||
const csvWithBOM = BOM + csvContent;
|
||||
|
||||
const blob = new Blob([csvWithBOM], { type: 'text/csv;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${fileName}_${Date.now()}.csv`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
showInfoMessage("Data exported successfully", "Export Data");
|
||||
modals.closeAll();
|
||||
};
|
||||
|
||||
modals.open({
|
||||
title: "Export Performance Test Data",
|
||||
centered: true,
|
||||
children: (
|
||||
<Stack>
|
||||
<Text>Export Performance Test Data</Text>
|
||||
<Group>
|
||||
<Button onClick={() => onClickExportJson(true)}>Download Json</Button>
|
||||
<Button onClick={() => onClickExportJson(false)}>Preview Json</Button>
|
||||
<Button onClick={() => onClickExportCsv()}>CSV</Button>
|
||||
</Group>
|
||||
<Group mt="md" justify="flex-end">
|
||||
<Button onClick={() => modals.closeAll()}>Cancel</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
144193
rest_grpc_compare_frontend/app/exapleData.ts
Normal file
144193
rest_grpc_compare_frontend/app/exapleData.ts
Normal file
File diff suppressed because it is too large
Load Diff
144227
rest_grpc_compare_frontend/app/exapleData_Local.ts
Normal file
144227
rest_grpc_compare_frontend/app/exapleData_Local.ts
Normal file
File diff suppressed because it is too large
Load Diff
80
rest_grpc_compare_frontend/app/pages/Dashboard.tsx
Normal file
80
rest_grpc_compare_frontend/app/pages/Dashboard.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import type {Route} from "../../.react-router/types/app/pages/+types/Dashboard";
|
||||
import {useDisclosure, useWindowScroll} from "@mantine/hooks";
|
||||
import {Affix, AppShell, Button, Space, Stack, Transition} from "@mantine/core";
|
||||
import {PageHeader} from "~/components/PageHeader.tsx";
|
||||
import {PageNavbar} from "~/components/PageNavbar.tsx";
|
||||
import {useEffect, useState} from "react";
|
||||
import {Outlet, useLocation, useNavigate} from "react-router";
|
||||
import {getEnumKeyByValue} from "~/utils/utils.ts";
|
||||
import {apiPing} from "~/utils/compare_api.ts";
|
||||
import ArrowUpIcon from "mdi-react/ArrowUpIcon";
|
||||
import HomeIcon from "mdi-react/HomeIcon";
|
||||
import GlobalAffix from "~/components/GlobalAffix.tsx";
|
||||
import {DashboardPageType} from "~/utils/enums.ts";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Dashboard" },
|
||||
{ name: "description", content: "Dashboard Main" },
|
||||
];
|
||||
}
|
||||
|
||||
export default function Component() {
|
||||
const navigate = useNavigate();
|
||||
const [currentStat, setCurrentStat] = useState(DashboardPageType.Home)
|
||||
const [opened, { toggle }] = useDisclosure()
|
||||
const location = useLocation();
|
||||
|
||||
const changePage = (pageType: DashboardPageType, navigateTo?: string) => {
|
||||
// if (currentStat == pageType) return
|
||||
setCurrentStat(pageType)
|
||||
navigate(navigateTo || pageType)
|
||||
}
|
||||
|
||||
const refreshMyInfo = () => {
|
||||
apiPing()
|
||||
.then((res => {
|
||||
if (!res.success) {
|
||||
navigate(DashboardPageType.Home)
|
||||
}
|
||||
}))
|
||||
.catch(err => {
|
||||
console.log("apiGetMyInfo failed", err)
|
||||
navigate(DashboardPageType.Home)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refreshMyInfo()
|
||||
let currPage = getEnumKeyByValue(DashboardPageType, location.pathname)
|
||||
if (currPage) {
|
||||
setCurrentStat(DashboardPageType[currPage])
|
||||
}
|
||||
else {
|
||||
const pathParts = location.pathname.split("/")
|
||||
const path = pathParts.slice(0, pathParts.length - 1).join("/")
|
||||
currPage = getEnumKeyByValue(DashboardPageType, path + "/")
|
||||
if (currPage) {
|
||||
setCurrentStat(DashboardPageType[currPage])
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AppShell header={{ height: 60 }} navbar={{width: 300, breakpoint: `sm`, collapsed: {mobile: !opened}}} padding="md">
|
||||
<AppShell.Header p="md">
|
||||
<PageHeader opened={opened} toggle={toggle} appendTitle={""}/>
|
||||
</AppShell.Header>
|
||||
|
||||
<AppShell.Navbar p="md">
|
||||
<PageNavbar currentStat={currentStat} changePageStat={changePage} />
|
||||
</AppShell.Navbar>
|
||||
|
||||
<AppShell.Main>
|
||||
<Outlet context={{ refreshMyInfo, changePage }} />
|
||||
<Space h={50} />
|
||||
<GlobalAffix />
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
117
rest_grpc_compare_frontend/app/pages/HomePage.tsx
Normal file
117
rest_grpc_compare_frontend/app/pages/HomePage.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import type {Route} from "../../.react-router/types/app/pages/+types/HomePage";
|
||||
import {flexLeftWeight, iconMStyle, marginRound, maxWidth} from "~/styles";
|
||||
import {
|
||||
Button,
|
||||
em,
|
||||
Flex,
|
||||
Group,
|
||||
Image,
|
||||
Input,
|
||||
MantineProvider,
|
||||
PasswordInput,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput
|
||||
} from "@mantine/core";
|
||||
import {useForm} from "@mantine/form";
|
||||
import {useEffect, useState} from "react";
|
||||
import AccountIcon from "mdi-react/AccountIcon";
|
||||
import LockIcon from "mdi-react/LockIcon";
|
||||
import {Link} from "react-router";
|
||||
import { useNavigate } from "react-router";
|
||||
import {apiPing, apiPingWithPassword} from "~/utils/compare_api.ts";
|
||||
import {showErrorMessage, showInfoMessage} from "~/utils/utils";
|
||||
import {DashboardPageType} from "~/utils/enums.ts";
|
||||
import GlobalAffix from "~/components/GlobalAffix.tsx";
|
||||
import WebIcon from "mdi-react/WebIcon";
|
||||
|
||||
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Login" },
|
||||
{ name: "description", content: "Login page" },
|
||||
];
|
||||
}
|
||||
|
||||
export default function Component() {
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
// serverAddress: 'http://127.0.0.1:9520',
|
||||
serverAddress: 'https://api-grpc-vs-rest.yanfeng.uk',
|
||||
password: ''
|
||||
},
|
||||
|
||||
validate: {
|
||||
serverAddress: (value) => (value.length == 0 ? "Input the server address." : null),
|
||||
password: (value) => (value.length == 0 ? "Input your password." : null),
|
||||
},
|
||||
});
|
||||
|
||||
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onClickLogin = (values: {serverAddress:string, password: string}) => {
|
||||
setIsLoggingIn(true)
|
||||
apiPingWithPassword(values.serverAddress, values.password)
|
||||
.then((res) => {
|
||||
if (!res.success) {
|
||||
showErrorMessage(res.message, "Login failed")
|
||||
return
|
||||
}
|
||||
else {
|
||||
navigate(DashboardPageType.Dashboard)
|
||||
}
|
||||
})
|
||||
.catch((e) => showErrorMessage(e.toString(), "Login error"))
|
||||
.finally(() => setIsLoggingIn(false))
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex gap="md" align="center" justify="center" p={em(20)} h="100vh" w="100vw">
|
||||
<Stack align="stretch" justify="center" gap="md" style={{...flexLeftWeight, maxWidth: "800px"}}>
|
||||
<Text size="1.5em" fw={700}>Log in</Text>
|
||||
|
||||
<form onSubmit={form.onSubmit((values) => onClickLogin(values))}>
|
||||
<Stack gap="xs">
|
||||
{/*<TextInput*/}
|
||||
{/* withAsterisk*/}
|
||||
{/* label="Username"*/}
|
||||
{/* placeholder="Username"*/}
|
||||
{/* leftSection={<AccountIcon style={iconMStyle}/>}*/}
|
||||
{/* {...form.getInputProps('userName')}*/}
|
||||
{/*/>*/}
|
||||
|
||||
<TextInput
|
||||
withAsterisk
|
||||
label="Test Server Address"
|
||||
placeholder="Server Address"
|
||||
leftSection={<WebIcon style={iconMStyle}/>}
|
||||
{...form.getInputProps('serverAddress')}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
withAsterisk
|
||||
label="Password"
|
||||
placeholder="Password"
|
||||
leftSection={<LockIcon style={iconMStyle}/>}
|
||||
{...form.getInputProps('password')}
|
||||
/>
|
||||
|
||||
<Text size="sm" c="dimmed" ta="right">
|
||||
Forgot Password?
|
||||
</Text>
|
||||
|
||||
<Group justify="space-between">
|
||||
{/*<Button onClick={() => navigate(DashboardPageType.RegisterPage)}>Register</Button>*/}
|
||||
<Button type="submit" disabled={isLoggingIn}>Login</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
|
||||
<GlobalAffix/>
|
||||
</Stack>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
21
rest_grpc_compare_frontend/app/pages/dashboard/About.tsx
Normal file
21
rest_grpc_compare_frontend/app/pages/dashboard/About.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import type {Route} from "../../../.react-router/types/app/pages/dashboard/+types/Home";
|
||||
import {Badge, Button, Card, em, Grid, Group, Image, Modal, PasswordInput, Stack, Table, Text} from "@mantine/core";
|
||||
import React from "react";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Home" },
|
||||
{ name: "description", content: "Dashboard Home" },
|
||||
];
|
||||
}
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Group justify="center" align="center">
|
||||
<Stack align="center" justify="center">
|
||||
<Text fw={700} size={em(28)}>Performance Test Server Architecture</Text>
|
||||
<Image src="/server_architecture.png" w={1200}/>
|
||||
</Stack>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
40
rest_grpc_compare_frontend/app/pages/dashboard/Home.tsx
Normal file
40
rest_grpc_compare_frontend/app/pages/dashboard/Home.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import type {Route} from "../../../.react-router/types/app/pages/dashboard/+types/Home";
|
||||
import {Badge, Button, Card, em, Grid, Group, Image, Modal, PasswordInput, Stack, Table, Text} from "@mantine/core";
|
||||
import React from "react";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Home" },
|
||||
{ name: "description", content: "Dashboard Home" },
|
||||
];
|
||||
}
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Group justify="center" align="center">
|
||||
<Stack align="center" justify="center">
|
||||
<Image src="/icon.png" w={400} h={400}/>
|
||||
<Text fw={700} size={em(28)}>Performance Comparison of Communication Protocols in Microservices</Text>
|
||||
<Text fw={500} size="xl">
|
||||
<Text
|
||||
size="xl"
|
||||
fw={900}
|
||||
variant="gradient"
|
||||
gradient={{ from: 'teal', to: 'lime', deg: 90 }}
|
||||
span
|
||||
>
|
||||
HTTP RESTful
|
||||
</Text> vs <Text
|
||||
size="xl"
|
||||
fw={900}
|
||||
variant="gradient"
|
||||
gradient={{ from: 'yellow', to: 'orange', deg: 90 }}
|
||||
span
|
||||
>
|
||||
gRPC
|
||||
</Text>
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
22
rest_grpc_compare_frontend/app/pages/dashboard/Page2.tsx
Normal file
22
rest_grpc_compare_frontend/app/pages/dashboard/Page2.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import type {Route} from "../../../.react-router/types/app/pages/dashboard/+types/Page2";
|
||||
import {useDisclosure} from "@mantine/hooks";
|
||||
import {useState} from "react";
|
||||
import {useNavigate} from "react-router";
|
||||
import { useOutletContext } from 'react-router';
|
||||
import type {OutletContextType} from "~/utils/models.ts";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Dashboard P2" },
|
||||
{ name: "description", content: "Dashboard P2" },
|
||||
];
|
||||
}
|
||||
|
||||
export default function Component() {
|
||||
const navigate = useNavigate();
|
||||
const { refreshMyInfo, changePage } = useOutletContext<OutletContextType>();
|
||||
|
||||
return (
|
||||
<h1>Dashboard Page2</h1>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
import type {Route} from "../../../.react-router/types/app/pages/dashboard/+types/PerformanceTestGetList";
|
||||
import React from "react";
|
||||
import type {PerformanceGetListData} from "~/utils/models.ts";
|
||||
import { proxy } from 'valtio';
|
||||
import PerformanceTest from "~/components/PerformanceTest";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Performance Test GetList" },
|
||||
{ name: "description", content: "Performance Test GetList" },
|
||||
];
|
||||
}
|
||||
|
||||
const performanceDataState = proxy<PerformanceGetListData[]>([]);
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<PerformanceTest isPushTest={false} performanceDataState={performanceDataState} />
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
import type {Route} from "../../../.react-router/types/app/pages/dashboard/+types/PerformanceTestPushData";
|
||||
import React from "react";
|
||||
import type {PerformanceGetListData} from "~/utils/models.ts";
|
||||
import { proxy } from 'valtio';
|
||||
import PerformanceTest from "~/components/PerformanceTest";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Performance Test PushData" },
|
||||
{ name: "description", content: "Performance Test PushData" },
|
||||
];
|
||||
}
|
||||
|
||||
const performanceDataState = proxy<PerformanceGetListData[]>([]);
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<PerformanceTest isPushTest={true} performanceDataState={performanceDataState} />
|
||||
)
|
||||
}
|
||||
96
rest_grpc_compare_frontend/app/root.tsx
Normal file
96
rest_grpc_compare_frontend/app/root.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import {
|
||||
isRouteErrorResponse,
|
||||
Links,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
} from "react-router";
|
||||
|
||||
import type { Route } from "./+types/root";
|
||||
import "./app.css";
|
||||
import '@mantine/core/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
import '@mantine/dates/styles.css';
|
||||
import '@mantine/charts/styles.css';
|
||||
|
||||
import { ColorSchemeScript, MantineProvider, mantineHtmlProps } from '@mantine/core';
|
||||
import {Notifications} from "@mantine/notifications";
|
||||
import {ModalsProvider} from "@mantine/modals";
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
import {theme} from "~/theme.ts";
|
||||
|
||||
dayjs.extend(customParseFormat);
|
||||
|
||||
|
||||
// export const links: Route.LinksFunction = () => [
|
||||
// { rel: "preconnect", href: "https://fonts.googleapis.com" },
|
||||
// {
|
||||
// rel: "preconnect",
|
||||
// href: "https://fonts.gstatic.com",
|
||||
// crossOrigin: "anonymous",
|
||||
// },
|
||||
// {
|
||||
// rel: "stylesheet",
|
||||
// href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
|
||||
// },
|
||||
// ];
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" {...mantineHtmlProps}>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<ColorSchemeScript />
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
<MantineProvider theme={theme}>
|
||||
<ModalsProvider>
|
||||
<Notifications position="top-center" zIndex={1000} />
|
||||
{children}
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||
let message = "Oops!";
|
||||
let details = "An unexpected error occurred.";
|
||||
let stack: string | undefined;
|
||||
|
||||
if (isRouteErrorResponse(error)) {
|
||||
message = error.status === 404 ? "404" : "Error";
|
||||
details =
|
||||
error.status === 404
|
||||
? "The requested page could not be found."
|
||||
: error.statusText || details;
|
||||
} else if (import.meta.env.DEV && error && error instanceof Error) {
|
||||
details = error.message;
|
||||
stack = error.stack;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="pt-16 p-4 container mx-auto">
|
||||
<h1>{message}</h1>
|
||||
<p>{details}</p>
|
||||
{stack && (
|
||||
<pre className="w-full p-4 overflow-x-auto">
|
||||
<code>{stack}</code>
|
||||
</pre>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
16
rest_grpc_compare_frontend/app/routes.ts
Normal file
16
rest_grpc_compare_frontend/app/routes.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { type RouteConfig, index, route, prefix } from "@react-router/dev/routes";
|
||||
|
||||
export default [
|
||||
index("pages/HomePage.tsx"),
|
||||
// route("page2", "pages/Page2.tsx"),
|
||||
|
||||
route("dashboard", "pages/Dashboard.tsx", [
|
||||
index("pages/dashboard/Home.tsx"),
|
||||
|
||||
route("test_get_list", "pages/dashboard/PerformanceTestGetList.tsx"),
|
||||
route("test_push_data", "pages/dashboard/PerformanceTestPushData.tsx"),
|
||||
route("about", "pages/dashboard/About.tsx"),
|
||||
|
||||
route("page2", "pages/dashboard/Page2.tsx"),
|
||||
]),
|
||||
] satisfies RouteConfig;
|
||||
53
rest_grpc_compare_frontend/app/styles.ts
Normal file
53
rest_grpc_compare_frontend/app/styles.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import type {CSSProperties} from "react";
|
||||
import {rem} from "@mantine/core";
|
||||
|
||||
export const maxWidth: CSSProperties = {
|
||||
width: "100%"
|
||||
}
|
||||
|
||||
export const marginTopBottom: CSSProperties = {
|
||||
marginTop: "1em",
|
||||
marginBottom: "1em"
|
||||
}
|
||||
|
||||
export const marginTop: CSSProperties = {
|
||||
marginTop: "1em",
|
||||
}
|
||||
|
||||
export const marginRightBottom: CSSProperties = {
|
||||
marginRight: "1em",
|
||||
marginBottom: "1em"
|
||||
}
|
||||
|
||||
export const marginRound: CSSProperties = {
|
||||
margin: "1em"
|
||||
}
|
||||
|
||||
export const marginLeftRight: CSSProperties = {
|
||||
marginLeft: "1em",
|
||||
marginRight: "1em"
|
||||
}
|
||||
|
||||
export const iconMStyle: CSSProperties = {
|
||||
width: rem(18),
|
||||
height: rem(18)
|
||||
}
|
||||
|
||||
export const flexLeftWeight: CSSProperties = {
|
||||
flex: 1
|
||||
}
|
||||
|
||||
export const textCenter: CSSProperties = {
|
||||
textAlign: 'center'
|
||||
}
|
||||
|
||||
export const noColumnGap: CSSProperties = {
|
||||
columnGap: "0"
|
||||
}
|
||||
|
||||
export const roundThemeButton: CSSProperties = {
|
||||
width: "55px",
|
||||
height: "55px",
|
||||
borderRadius: "55px",
|
||||
padding: "0"
|
||||
}
|
||||
5
rest_grpc_compare_frontend/app/theme.ts
Normal file
5
rest_grpc_compare_frontend/app/theme.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { createTheme } from "@mantine/core";
|
||||
|
||||
export const theme = createTheme({
|
||||
|
||||
});
|
||||
54
rest_grpc_compare_frontend/app/utils/compare_api.ts
Normal file
54
rest_grpc_compare_frontend/app/utils/compare_api.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import type {
|
||||
BaseRetData, PerformanceGetListData, PerformanceGetListResponse,
|
||||
} from "./models"
|
||||
import {sleep} from "~/utils/utils.ts";
|
||||
|
||||
// export const apiEndpoint = import.meta.env.VITE_API_ENDPOINT
|
||||
|
||||
|
||||
export async function fetchAPI(path: string, method: string, body?: any, headers?: any, contentType: string | null = "application/json"): Promise<Response> {
|
||||
const baseHeaders: { [key: string]: any } = {
|
||||
"password": `${localStorage.getItem("password")}`,
|
||||
}
|
||||
if (contentType) {
|
||||
baseHeaders["Content-Type"] = contentType
|
||||
}
|
||||
let reqHeaders: any
|
||||
if (headers) {
|
||||
reqHeaders = {...baseHeaders, ...headers}
|
||||
}
|
||||
else {
|
||||
reqHeaders = baseHeaders
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(`${localStorage.getItem("testServerAddress")}/${path}`, {
|
||||
method,
|
||||
credentials: "include",
|
||||
headers: reqHeaders,
|
||||
body: contentType === "application/json" ? (body ? JSON.stringify(body) : undefined) : body,
|
||||
})
|
||||
.then((v) => resolve(v))
|
||||
.catch((e) => reject(e))
|
||||
})
|
||||
}
|
||||
|
||||
export async function apiPing(): Promise<BaseRetData> {
|
||||
const resp = await fetchAPI("ping", "GET")
|
||||
return resp.json()
|
||||
}
|
||||
|
||||
export async function apiPingWithPassword(serverAddress: string, password: string): Promise<BaseRetData> {
|
||||
localStorage.setItem("testServerAddress", serverAddress)
|
||||
localStorage.setItem("password", password)
|
||||
return apiPing()
|
||||
}
|
||||
|
||||
export async function apiPerformanceGetList(count: number, example_data_limit: number, idx: number): Promise<PerformanceGetListResponse> {
|
||||
const resp = await fetchAPI(`performance_test_get_list?count=${count}&example_data_limit=${example_data_limit}&idx=${idx}`, "GET")
|
||||
return resp.json()
|
||||
}
|
||||
|
||||
export async function apiPerformanceUploadData(count: number, idx: number): Promise<PerformanceGetListResponse> {
|
||||
const resp = await fetchAPI(`performance_test_add_books?count=${count}&idx=${idx}`, "GET")
|
||||
return resp.json()
|
||||
}
|
||||
7
rest_grpc_compare_frontend/app/utils/enums.ts
Normal file
7
rest_grpc_compare_frontend/app/utils/enums.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export enum DashboardPageType {
|
||||
Home = "/",
|
||||
About = "/dashboard/about",
|
||||
Dashboard = "/dashboard",
|
||||
DashboardTestGetList = "/dashboard/test_get_list",
|
||||
DashboardTestPushData = "/dashboard/test_push_data",
|
||||
}
|
||||
71
rest_grpc_compare_frontend/app/utils/models.ts
Normal file
71
rest_grpc_compare_frontend/app/utils/models.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import type {DashboardPageType} from "~/utils/enums.ts";
|
||||
|
||||
interface IKeyString {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface BaseRetData extends IKeyString {
|
||||
success: boolean,
|
||||
message: string,
|
||||
data?: any
|
||||
}
|
||||
|
||||
export interface PerformanceGetListResponse extends BaseRetData {
|
||||
data?: PerformanceGetListData
|
||||
}
|
||||
|
||||
export interface PerformanceGetListData extends BaseRetData {
|
||||
idx: number,
|
||||
request_count: number,
|
||||
grpc: PerformanceResultsGetList,
|
||||
rest: PerformanceResultsGetList,
|
||||
}
|
||||
|
||||
export interface PerformanceResultsGetList extends PerformanceResults {
|
||||
response_data?: BookInfo[]
|
||||
}
|
||||
|
||||
export interface PerformanceResults extends IKeyString {
|
||||
client_networking_ping: number,
|
||||
client_request_cpu: number,
|
||||
client_request_time: number,
|
||||
server_deserialize_cpu: number,
|
||||
server_deserialize_time: number,
|
||||
server_serialize_cpu: number,
|
||||
server_serialize_time: number,
|
||||
server_protocol_total_time: number,
|
||||
networking_size: {
|
||||
request_size: number,
|
||||
response_size: number
|
||||
},
|
||||
response_data?: any
|
||||
}
|
||||
|
||||
export interface BookInfo extends IKeyString {
|
||||
"abstract": string
|
||||
"author": string
|
||||
"barcode": string
|
||||
"binding": string
|
||||
"category_id": number,
|
||||
"cover_image": string
|
||||
"description": string
|
||||
"edition": string
|
||||
"editor": string
|
||||
"format": string
|
||||
"isbn": string
|
||||
"keywords": string
|
||||
"language": string
|
||||
"pages": number,
|
||||
"publication_date": string
|
||||
"publisher": string
|
||||
"subject": string
|
||||
"subtitle": string
|
||||
"title": string
|
||||
"translator": string
|
||||
"weight": number
|
||||
}
|
||||
|
||||
export interface OutletContextType {
|
||||
refreshMyInfo: () => void;
|
||||
changePage: (pageType: DashboardPageType, navigateTo?: string) => void;
|
||||
}
|
||||
58
rest_grpc_compare_frontend/app/utils/utils.ts
Normal file
58
rest_grpc_compare_frontend/app/utils/utils.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import {notifications} from "@mantine/notifications";
|
||||
|
||||
export function showErrorMessage(msg: string, title: string = "Error", autoClose: boolean | number = 10000) {
|
||||
console.log("ErrorMessage:", title, msg)
|
||||
notifications.show({
|
||||
title: title,
|
||||
message: msg,
|
||||
color: 'red',
|
||||
autoClose: autoClose
|
||||
})
|
||||
}
|
||||
|
||||
export function showWarningMessage(msg: string, title: string = "Warning", autoClose: boolean | number = 10000) {
|
||||
console.log("WarningMessage:", title, msg)
|
||||
notifications.show({
|
||||
title: title,
|
||||
message: msg,
|
||||
color: 'yellow',
|
||||
autoClose: autoClose,
|
||||
})
|
||||
}
|
||||
|
||||
export function showInfoMessage(msg: string, title: string = "Info", autoClose: boolean | number = 10000) {
|
||||
notifications.show({
|
||||
title: title,
|
||||
message: msg,
|
||||
color: 'blue',
|
||||
autoClose: autoClose,
|
||||
})
|
||||
}
|
||||
|
||||
export function getEnumKeyByValue<T extends Record<string, string>>(
|
||||
enumObj: T,
|
||||
value: string
|
||||
): keyof T | undefined {
|
||||
return (Object.keys(enumObj) as Array<keyof T>)
|
||||
.find(k => enumObj[k] === value);
|
||||
}
|
||||
|
||||
|
||||
export function convertNumber(value: number, multiplier: number, decimalPlaces: number) {
|
||||
if (!Number.isFinite(value) || !Number.isFinite(multiplier)) {
|
||||
return "NaN"
|
||||
}
|
||||
|
||||
if (!Number.isInteger(decimalPlaces) || decimalPlaces < 0) {
|
||||
return "NaN"
|
||||
}
|
||||
|
||||
const result = value * multiplier;
|
||||
|
||||
return result.toFixed(decimalPlaces);
|
||||
}
|
||||
|
||||
|
||||
export const sleep = (ms: number): Promise<void> => {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
};
|
||||
6634
rest_grpc_compare_frontend/package-lock.json
generated
Normal file
6634
rest_grpc_compare_frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
46
rest_grpc_compare_frontend/package.json
Normal file
46
rest_grpc_compare_frontend/package.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "rest-grpc-compare",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "react-router build",
|
||||
"dev": "react-router dev",
|
||||
"start": "react-router-serve ./build/server/index.js",
|
||||
"typecheck": "react-router typegen && tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mantine/charts": "^7.17.4",
|
||||
"@mantine/core": "^7.17.4",
|
||||
"@mantine/dates": "^7.17.4",
|
||||
"@mantine/form": "^7.17.4",
|
||||
"@mantine/hooks": "^7.17.4",
|
||||
"@mantine/modals": "^7.17.4",
|
||||
"@mantine/notifications": "^7.17.4",
|
||||
"@react-router/node": "^7.5.0",
|
||||
"@react-router/serve": "^7.5.0",
|
||||
"@vitejs/plugin-react": "^4.4.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"isbot": "^5.1.17",
|
||||
"mdi-react": "^9.4.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router": "^7.5.0",
|
||||
"recharts": "^2.15.4",
|
||||
"valtio": "^2.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-router/dev": "^7.5.0",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19.0.1",
|
||||
"@types/react-dom": "^19.0.1",
|
||||
"react-router-devtools": "^1.1.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^5.4.11",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
},
|
||||
"overrides": {
|
||||
"react-is": "^19.0.0"
|
||||
}
|
||||
}
|
||||
BIN
rest_grpc_compare_frontend/public/favicon.ico
Normal file
BIN
rest_grpc_compare_frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
BIN
rest_grpc_compare_frontend/public/icon.png
Normal file
BIN
rest_grpc_compare_frontend/public/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
BIN
rest_grpc_compare_frontend/public/server_architecture.png
Normal file
BIN
rest_grpc_compare_frontend/public/server_architecture.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
7
rest_grpc_compare_frontend/react-router.config.ts
Normal file
7
rest_grpc_compare_frontend/react-router.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { Config } from "@react-router/dev/config";
|
||||
|
||||
export default {
|
||||
// Config options...
|
||||
// Server-side render by default, to enable SPA mode set this to `false`
|
||||
ssr: false,
|
||||
} satisfies Config;
|
||||
33
rest_grpc_compare_frontend/tsconfig.json
Normal file
33
rest_grpc_compare_frontend/tsconfig.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"include": [
|
||||
"**/*",
|
||||
"**/.server/**/*",
|
||||
"**/.client/**/*",
|
||||
".react-router/types/**/*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"types": ["node", "vite/client"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"rootDirs": [".", "./.react-router/types"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./app/*"]
|
||||
},
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
}
|
||||
}
|
||||
8
rest_grpc_compare_frontend/vite.config.ts
Normal file
8
rest_grpc_compare_frontend/vite.config.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { reactRouter } from "@react-router/dev/vite";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { defineConfig } from "vite";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user