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