@@ -63,7 +63,7 @@ target_compile_definitions(lidar_manager_web PRIVATE
|
|||||||
_DEFAULT_SOURCE
|
_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)
|
if(BUILD_TESTING)
|
||||||
enable_testing()
|
enable_testing()
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ WORKDIR /src
|
|||||||
COPY CMakeLists.txt ./
|
COPY CMakeLists.txt ./
|
||||||
COPY src ./src
|
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)"
|
&& cmake --build build -j"$(nproc)"
|
||||||
|
|
||||||
FROM ubuntu:20.04 AS runtime
|
FROM ubuntu:20.04 AS runtime
|
||||||
|
|||||||
@@ -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`).
|
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`.
|
CI: GitHub Actions workflow `.github/workflows/test.yml`.
|
||||||
|
|
||||||
|
|||||||
@@ -1,51 +1,11 @@
|
|||||||
{
|
{
|
||||||
"queue": [
|
"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"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"runner": {
|
"runner": {
|
||||||
"current_action": null,
|
"current_action": null,
|
||||||
"current_queue_id": null,
|
"current_queue_id": null,
|
||||||
"message": "Hoàn thành: Test run",
|
"message": "",
|
||||||
"paused": false,
|
"paused": false,
|
||||||
"state": "idle",
|
"state": "idle",
|
||||||
"updated_at": "2026-06-13T06:34:15Z"
|
"updated_at": "2026-06-13T06:56:22Z"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
93
scripts/benchmark-http.sh
Executable file
93
scripts/benchmark-http.sh
Executable file
@@ -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
|
||||||
125
scripts/docker-benchmark.sh
Normal file
125
scripts/docker-benchmark.sh
Normal file
@@ -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"
|
||||||
@@ -11,6 +11,15 @@ BIN="${ROOT}/build/lidar_manager_web"
|
|||||||
DATA_DIR="$(mktemp -d)"
|
DATA_DIR="$(mktemp -d)"
|
||||||
SERVER_PID=""
|
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() {
|
cleanup() {
|
||||||
if [[ -n "$SERVER_PID" ]] && kill -0 "$SERVER_PID" 2>/dev/null; then
|
if [[ -n "$SERVER_PID" ]] && kill -0 "$SERVER_PID" 2>/dev/null; then
|
||||||
kill "$SERVER_PID" 2>/dev/null || true
|
kill "$SERVER_PID" 2>/dev/null || true
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
#include "util/file_util.hpp"
|
#include "util/file_util.hpp"
|
||||||
#include "util/id_util.hpp"
|
#include "util/id_util.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
@@ -97,12 +96,6 @@ void MissionQueue::ensureRunnerDefaults()
|
|||||||
runner_["paused"] = false;
|
runner_["paused"] = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MissionQueue::saveLocked() const
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(mu_);
|
|
||||||
saveUnlocked();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MissionQueue::startWorkerIfNeeded()
|
void MissionQueue::startWorkerIfNeeded()
|
||||||
{
|
{
|
||||||
if (worker_.joinable())
|
if (worker_.joinable())
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ private:
|
|||||||
const nlohmann::json& parameters,
|
const nlohmann::json& parameters,
|
||||||
nlohmann::json& log,
|
nlohmann::json& log,
|
||||||
int loop_depth);
|
int loop_depth);
|
||||||
void saveLocked() const;
|
|
||||||
void sleepMs(int ms);
|
void sleepMs(int ms);
|
||||||
void setRunnerState(const std::string& state, const std::string& message = "");
|
void setRunnerState(const std::string& state, const std::string& message = "");
|
||||||
void insertByPriorityUnlocked(nlohmann::json& entry);
|
void insertByPriorityUnlocked(nlohmann::json& entry);
|
||||||
|
|||||||
@@ -55,11 +55,6 @@ nlohmann::json ModbusTriggerService::coilStates() const
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ModbusTriggerService::onCoilRisingEdgeUnlocked(int coil_id)
|
|
||||||
{
|
|
||||||
(void)coil_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ModbusTriggerService::writeCoil(int coil_id, bool value, std::string& err)
|
bool ModbusTriggerService::writeCoil(int coil_id, bool value, std::string& err)
|
||||||
{
|
{
|
||||||
if (!coilIdValid(coil_id))
|
if (!coilIdValid(coil_id))
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ private:
|
|||||||
std::atomic<bool> stop_{false};
|
std::atomic<bool> stop_{false};
|
||||||
std::thread tcp_thread_;
|
std::thread tcp_thread_;
|
||||||
|
|
||||||
void onCoilRisingEdgeUnlocked(int coil_id);
|
|
||||||
void tcpLoop();
|
void tcpLoop();
|
||||||
void handleTcpClient(int client_fd);
|
void handleTcpClient(int client_fd);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,15 +5,6 @@
|
|||||||
|
|
||||||
namespace lm {
|
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)
|
bool ApiServer::enqueueRequest(const nlohmann::json& request, httplib::Response& res, int status_code)
|
||||||
{
|
{
|
||||||
nlohmann::json payload;
|
nlohmann::json payload;
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user