From fbc0c11be2dd39b577af539099c462b3bcfa8fb9 Mon Sep 17 00:00:00 2001 From: HiepLM Date: Sat, 13 Jun 2026 13:58:38 +0700 Subject: [PATCH] =?UTF-8?q?Test=20l=E1=BA=A7n=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 2 +- Dockerfile | 2 +- README.md | 7 + data/mission_queue.json | 46 +------ scripts/benchmark-http.sh | 93 +++++++++++++ scripts/docker-benchmark.sh | 125 ++++++++++++++++++ scripts/run-tests.sh | 9 ++ src/mission/mission_queue.cpp | 7 - src/mission/mission_queue.hpp | 1 - src/mission/modbus_trigger_service.cpp | 5 - src/mission/modbus_trigger_service.hpp | 1 - src/server/api_mission_routes.cpp | 9 -- ...pi_integration.cpython-38-pytest-8.3.5.pyc | Bin 9563 -> 9563 bytes 13 files changed, 239 insertions(+), 68 deletions(-) create mode 100755 scripts/benchmark-http.sh create mode 100644 scripts/docker-benchmark.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index db2baf4..179aaf1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,7 +63,7 @@ target_compile_definitions(lidar_manager_web PRIVATE _DEFAULT_SOURCE ) -option(BUILD_TESTING "Build unit and integration test helpers" ON) +option(BUILD_TESTING "Build unit tests (requires tests/ directory)" OFF) if(BUILD_TESTING) enable_testing() diff --git a/Dockerfile b/Dockerfile index d66fe6a..585aa5e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ WORKDIR /src COPY CMakeLists.txt ./ COPY src ./src -RUN cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ +RUN cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF \ && cmake --build build -j"$(nproc)" FROM ubuntu:20.04 AS runtime diff --git a/README.md b/README.md index 099f501..cfab678 100644 --- a/README.md +++ b/README.md @@ -101,5 +101,12 @@ Chỉ API smoke (server đang chạy, dùng fixture `tests/fixtures/data/`): Fixture mission id mặc định: `testmission00001` (`tests/fixtures/data/missions.json`). +Benchmark hiệu năng trong container (cần `docker compose up -d`): + +```bash +chmod +x scripts/docker-benchmark.sh +sudo ./scripts/docker-benchmark.sh +``` + CI: GitHub Actions workflow `.github/workflows/test.yml`. diff --git a/data/mission_queue.json b/data/mission_queue.json index 6a662ed..2385525 100644 --- a/data/mission_queue.json +++ b/data/mission_queue.json @@ -1,51 +1,11 @@ { - "queue": [ - { - "created_at": "2026-06-13T06:34:14Z", - "finished_at": "2026-06-13T06:34:15Z", - "id": "e164539b35bf3886", - "log": [ - { - "level": "info", - "message": "Wait 1000ms", - "ts": "2026-06-13T06:34:14Z" - } - ], - "mission": { - "actions": [ - { - "id": "a1", - "kind": "action", - "label": "Wait", - "params": { - "seconds": 1 - }, - "type": "wait" - } - ], - "description": "", - "group": "Missions", - "id": "5ae9dbcb0722dffb", - "name": "Test run", - "updated_at": "2026-06-13T04:44:03Z" - }, - "mission_group": "Missions", - "mission_id": "5ae9dbcb0722dffb", - "mission_name": "Test run", - "parameters": {}, - "priority": 0, - "robot_id": "default", - "source": "rest_api_v2", - "started_at": "2026-06-13T06:34:14Z", - "status": "completed" - } - ], + "queue": [], "runner": { "current_action": null, "current_queue_id": null, - "message": "Hoàn thành: Test run", + "message": "", "paused": false, "state": "idle", - "updated_at": "2026-06-13T06:34:15Z" + "updated_at": "2026-06-13T06:56:22Z" } } \ No newline at end of file diff --git a/scripts/benchmark-http.sh b/scripts/benchmark-http.sh new file mode 100755 index 0000000..d0d96a4 --- /dev/null +++ b/scripts/benchmark-http.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# Benchmark HTTP latency + tài nguyên process (local hoặc container qua URL). +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +BASE="${1:-http://127.0.0.1:8080}" +REQUESTS="${BENCH_REQUESTS:-100}" +WARMUP="${BENCH_WARMUP:-10}" +LABEL="${2:-}" + +echo "=== lidar_manager_web benchmark ===" +echo "URL: $BASE" +echo "Requests/endpoint: $REQUESTS (warmup $WARMUP)" +[[ -n "$LABEL" ]] && echo "Label: $LABEL" +echo + +if ! curl -sf "$BASE/api/health" >/dev/null; then + echo "Server không phản hồi tại $BASE" >&2 + exit 1 +fi + +bench_endpoint() { + local name="$1" + local method="$2" + local path="$3" + local body="${4:-}" + python3 - "$name" "$method" "$BASE$path" "$REQUESTS" "$WARMUP" "$body" <<'PY' +import statistics +import sys +import time +import urllib.error +import urllib.request + +name, method, url, n_req, n_warm, body = sys.argv[1:7] +n_req = int(n_req) +n_warm = int(n_warm) +data = body.encode() if body else None +headers = {"Content-Type": "application/json"} if data else {} + +def once(): + req = urllib.request.Request(url, data=data, headers=headers, method=method) + t0 = time.perf_counter() + with urllib.request.urlopen(req, timeout=10) as resp: + resp.read() + return (time.perf_counter() - t0) * 1000.0 + +for _ in range(n_warm): + try: + once() + except Exception: + pass + +samples = [] +errors = 0 +for _ in range(n_req): + try: + samples.append(once()) + except Exception as exc: + errors += 1 + +if not samples: + print(f"{name}: FAIL errors={errors}") +else: + samples.sort() + def pct(p): + i = max(0, min(len(samples) - 1, int(len(samples) * p / 100.0) - 1)) + return samples[i] + print( + f"{name}: ok={len(samples)} err={errors} " + f"p50={pct(50):.2f}ms p95={pct(95):.2f}ms avg={statistics.mean(samples):.2f}ms max={samples[-1]:.2f}ms" + ) +PY +} + +echo "=== HTTP latency ===" +bench_endpoint "GET /api/health" GET "/api/health" +bench_endpoint "GET /api/state" GET "/api/state" +bench_endpoint "GET /api/missions" GET "/api/missions" +bench_endpoint "GET /api/mission_queue" GET "/api/mission_queue" +bench_endpoint "GET /api/v2.0.0/mission_queue" GET "/api/v2.0.0/mission_queue" +bench_endpoint "GET /" GET "/" +bench_endpoint "GET /missions.js" GET "/missions.js" + +MID="$(curl -sf "$BASE/api/missions" | python3 -c "import json,sys; m=json.load(sys.stdin).get('missions',[]); print(m[0]['id'] if m else '')" 2>/dev/null || true)" +if [[ -n "$MID" ]]; then + bench_endpoint "POST /api/v2.0.0/mission_queue" POST "/api/v2.0.0/mission_queue" \ + "{\"mission_id\":\"$MID\",\"priority\":0}" +fi + +echo +echo "=== Process lidar_manager_web ===" +ps -C lidar_manager_web -o pid,rss,vsz,pcpu,pmem,etime,cmd 2>/dev/null \ + || pgrep -af '[./]lidar_manager_web' | grep -v pgrep || true diff --git a/scripts/docker-benchmark.sh b/scripts/docker-benchmark.sh new file mode 100644 index 0000000..4f3b465 --- /dev/null +++ b/scripts/docker-benchmark.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# Hiệu năng lidar_manager_web trong container Docker. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# shellcheck source=docker-lib.sh +source "$ROOT/scripts/docker-lib.sh" + +docker_cmd + +NAME="${1:-lidar-manager-limited}" +BASE="${2:-http://127.0.0.1:8080}" +REQUESTS="${BENCH_REQUESTS:-100}" +WARMUP="${BENCH_WARMUP:-10}" + +if ! "${DOCKER[@]}" ps --format '{{.Names}}' | grep -qx "$NAME"; then + echo "Container '$NAME' không chạy. Chạy: sudo docker compose up -d --build" >&2 + exit 1 +fi + +echo "=== PhenikaaX lidar_manager_web — benchmark container ===" +echo "Container: $NAME" +echo "URL: $BASE" +echo "Requests/endpoint: $REQUESTS (warmup $WARMUP)" +echo + +echo "=== Docker stats (snapshot) ===" +"${DOCKER[@]}" stats --no-stream --format \ + 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}' "$NAME" +echo +print_container_limits "$NAME" +echo + +echo "=== Process trong container ===" +"${DOCKER[@]}" exec "$NAME" sh -c ' + echo "PID RSS_KB VSZ_KB CMD" + ps -o pid,rss,vsz,cmd -C lidar_manager_web 2>/dev/null || ps aux | grep lidar_manager_web | grep -v grep + echo + echo "--- /proc/meminfo (head) ---" + head -5 /proc/meminfo + echo "--- nproc ---" + nproc +' +echo + +bench_endpoint() { + local label="$1" + local method="$2" + local path="$3" + local body="${4:-}" + + python3 - "$label" "$method" "$BASE$path" "$REQUESTS" "$WARMUP" "$body" <<'PY' +import json +import statistics +import sys +import time +import urllib.error +import urllib.request + +label, method, url, n_req, n_warm, body = sys.argv[1:7] +n_req = int(n_req) +n_warm = int(n_warm) +data = body.encode() if body else None +headers = {"Content-Type": "application/json"} if data else {} + +def once(): + req = urllib.request.Request(url, data=data, headers=headers, method=method) + t0 = time.perf_counter() + with urllib.request.urlopen(req, timeout=10) as resp: + resp.read() + return (time.perf_counter() - t0) * 1000.0 + +for _ in range(n_warm): + try: + once() + except Exception: + pass + +samples = [] +errors = 0 +for _ in range(n_req): + try: + samples.append(once()) + except Exception: + errors += 1 + +if not samples: + print(f"{label}: FAIL (no successful samples, errors={errors})") +else: + samples.sort() + def pct(p): + i = max(0, min(len(samples) - 1, int(len(samples) * p / 100.0) - 1)) + return samples[i] + print( + f"{label}: ok={len(samples)} err={errors} " + f"min={samples[0]:.2f}ms p50={pct(50):.2f}ms p95={pct(95):.2f}ms " + f"max={samples[-1]:.2f}ms avg={statistics.mean(samples):.2f}ms" + ) +PY +} + +echo "=== HTTP latency (ms) ===" +if ! curl -sf "$BASE/api/health" >/dev/null; then + echo "Server không phản hồi tại $BASE" >&2 + exit 1 +fi + +bench_endpoint "GET /api/health" GET "/api/health" +bench_endpoint "GET /api/state" GET "/api/state" +bench_endpoint "GET /api/missions" GET "/api/missions" +bench_endpoint "GET /api/mission_queue" GET "/api/mission_queue" +bench_endpoint "GET /api/v2.0.0/mission_queue" GET "/api/v2.0.0/mission_queue" +bench_endpoint "GET / (index.html)" GET "/" +bench_endpoint "GET /missions.js" GET "/missions.js" + +MID="$(curl -sf "$BASE/api/missions" | python3 -c "import json,sys; m=json.load(sys.stdin).get('missions',[]); print(m[0]['id'] if m else '')")" +if [[ -n "$MID" ]]; then + bench_endpoint "POST /api/v2.0.0/mission_queue" POST "/api/v2.0.0/mission_queue" \ + "{\"mission_id\":\"$MID\",\"priority\":0}" +fi + +echo +echo "=== Docker stats (sau benchmark) ===" +"${DOCKER[@]}" stats --no-stream --format \ + 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}' "$NAME" diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh index bd5bb60..443494e 100755 --- a/scripts/run-tests.sh +++ b/scripts/run-tests.sh @@ -11,6 +11,15 @@ BIN="${ROOT}/build/lidar_manager_web" DATA_DIR="$(mktemp -d)" SERVER_PID="" +# Dọn server test cũ trên cùng port (nếu còn). +if command -v fuser >/dev/null 2>&1; then + fuser -k "${PORT}/tcp" 2>/dev/null || true +elif command -v lsof >/dev/null 2>&1; then + PIDS="$(lsof -ti "tcp:${PORT}" 2>/dev/null || true)" + [[ -n "$PIDS" ]] && kill $PIDS 2>/dev/null || true +fi +sleep 0.2 + cleanup() { if [[ -n "$SERVER_PID" ]] && kill -0 "$SERVER_PID" 2>/dev/null; then kill "$SERVER_PID" 2>/dev/null || true diff --git a/src/mission/mission_queue.cpp b/src/mission/mission_queue.cpp index 3d09591..2418b9e 100644 --- a/src/mission/mission_queue.cpp +++ b/src/mission/mission_queue.cpp @@ -3,7 +3,6 @@ #include "util/file_util.hpp" #include "util/id_util.hpp" -#include #include #include #include @@ -97,12 +96,6 @@ void MissionQueue::ensureRunnerDefaults() runner_["paused"] = false; } -void MissionQueue::saveLocked() const -{ - std::lock_guard lock(mu_); - saveUnlocked(); -} - void MissionQueue::startWorkerIfNeeded() { if (worker_.joinable()) diff --git a/src/mission/mission_queue.hpp b/src/mission/mission_queue.hpp index 79c74ba..787983c 100644 --- a/src/mission/mission_queue.hpp +++ b/src/mission/mission_queue.hpp @@ -51,7 +51,6 @@ private: const nlohmann::json& parameters, nlohmann::json& log, int loop_depth); - void saveLocked() const; void sleepMs(int ms); void setRunnerState(const std::string& state, const std::string& message = ""); void insertByPriorityUnlocked(nlohmann::json& entry); diff --git a/src/mission/modbus_trigger_service.cpp b/src/mission/modbus_trigger_service.cpp index 668b6ea..5c1e6f6 100644 --- a/src/mission/modbus_trigger_service.cpp +++ b/src/mission/modbus_trigger_service.cpp @@ -55,11 +55,6 @@ nlohmann::json ModbusTriggerService::coilStates() const return out; } -void ModbusTriggerService::onCoilRisingEdgeUnlocked(int coil_id) -{ - (void)coil_id; -} - bool ModbusTriggerService::writeCoil(int coil_id, bool value, std::string& err) { if (!coilIdValid(coil_id)) diff --git a/src/mission/modbus_trigger_service.hpp b/src/mission/modbus_trigger_service.hpp index 1c8776d..5ffd7bf 100644 --- a/src/mission/modbus_trigger_service.hpp +++ b/src/mission/modbus_trigger_service.hpp @@ -40,7 +40,6 @@ private: std::atomic stop_{false}; std::thread tcp_thread_; - void onCoilRisingEdgeUnlocked(int coil_id); void tcpLoop(); void handleTcpClient(int client_fd); }; diff --git a/src/server/api_mission_routes.cpp b/src/server/api_mission_routes.cpp index 18edcf8..a83fff1 100644 --- a/src/server/api_mission_routes.cpp +++ b/src/server/api_mission_routes.cpp @@ -5,15 +5,6 @@ namespace lm { -namespace { - -nlohmann::json mirError(const std::string& msg) -{ - return nlohmann::json{{"error", msg}, {"error_code", 400}}; -} - -} // namespace - bool ApiServer::enqueueRequest(const nlohmann::json& request, httplib::Response& res, int status_code) { nlohmann::json payload; diff --git a/tests/__pycache__/test_api_integration.cpython-38-pytest-8.3.5.pyc b/tests/__pycache__/test_api_integration.cpython-38-pytest-8.3.5.pyc index 03f025dcdc2b57cd5576815e2cda17647fbe5983..0a8fa53d73eee09dbe51646e4c46bd19dadde62a 100644 GIT binary patch delta 19 ZcmccZb=!+8l$V!_0SGqy-N+TF3IIG@1*`x7 delta 19 ZcmccZb=!+8l$V!_0SK1;-pCcG3IIG71)u-`