Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup toolchain
run: |
Expand All @@ -33,6 +35,10 @@ jobs:
make CROSSCOMPILER="$CROSSCOMPILER"
mv plotop plotop-${{ matrix.arch }}

- name: Verify version
run: |
./plotop-${{ matrix.arch }} --version

- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
Expand All @@ -55,6 +61,19 @@ jobs:
fetch-depth: 0
ref: ${{ github.ref }}

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'

- name: Install plotop
run: |
pip install .

- name: Verify Python package version
run: |
plotop --version

- name: Download Artifact
uses: actions/download-artifact@v4

Expand Down
30 changes: 15 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,30 @@

开发Plotop的主要目的是解决在嵌入式系统上做性能统计的困难。以往,我们通过top在嵌入式系统上观察性能数据,但是top信息往往是一小段时间内的快照,无法判断长时间运行的性能数据。针对这个问题,我开发过一些top解析脚本,通过将top信息写入文件,然后再解析问题,但是这种方案不尽我意。

Plotop由一个轻量的client和一个server组成。client负责采集性能数据并发送到server,client仅做简单的数据采集,避免对被测系统的影响。server可以一拖多用,支持多台client同时采集数据。server提供了一个web界面,可以实时查看性能数据,可以做数据分析,统计一段时间的性能数据。
Plotop由一个**轻量**的client和一个server组成。client负责采集性能数据并发送到server,client仅做简单的数据采集,避免对被测系统的影响。server可以一拖多用,支持多台client同时采集数据。server提供了一个web界面,可以实时查看性能数据,可以做数据分析,统计一段时间的性能数据。

![demo](statics/demo.png)

当前已实现以下核心组件:

- **C++17 客户端**:实现基础网络数据采集
- **C++17 客户端**:实现基础网络数据采集(TCP),仅做采集,确保client对host的影响小
- **Python Flask 服务端**:实现数据接收和存储
- **Web界面**:实时数据图表展示

### 主要功能

基础HTTP数据采集
基础TCP数据采集
✅ 简单数据缓存处理
✅ 静态图表展示
✅ 支持按照pid或进程名过滤数据
🚧 可视化配置界面(开发中)
✅ 支持按照pid或进程名过滤数据(Web端)
可视化配置界面

### 快速开始

#### 环境要求

- Python 3.8+
- C++17 兼容编译器
- CMake 3.12+

#### 安装步骤

Expand All @@ -46,14 +45,17 @@ pip install -r requirements.txt

# 编译客户端
make

# 启动交叉编译
CROSSCOMPILER=aarch64-linux-gnu- make
```

或者

通过pip安装服务端
通过pipx安装服务端

```bash
pip install git+https://github.com/caibingcheng/plotop.git
pipx install git+https://github.com/caibingcheng/plotop.git
```

从[https://github.com/caibingcheng/plotop/releases/latest](https://github.com/caibingcheng/plotop/releases/latest)下载最新的客户端。
Expand All @@ -63,19 +65,17 @@ pip install git+https://github.com/caibingcheng/plotop.git
```bash
# 启动服务端
python -m server
# plotop
# 或者通过 pipx 安装后
plotop

# 运行客户端
./plotop -i <server_ip> -p <server_port> -d <interval_seconds>

# 按进程名过滤
./plotop -i <server_ip> -p <server_port> -d <interval_seconds> -P <process_name>

# 按 PID 过滤(可多次指定)
./plotop -i <server_ip> -p <server_port> -d <interval_seconds> -I <pid1> -I <pid2>

# 查看帮助
./plotop --help

# 在 Web 界面按进程名或 PID 过滤数据
# 打开 http://<server_ip>:5000/?ip=<client_ip>,点击 "Select Processes"
```

### 项目结构
Expand Down
21 changes: 21 additions & 0 deletions client/cmdline.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
#include <unordered_map>
#include <vector>

#ifndef PLOTOP_VERSION
#define PLOTOP_VERSION "unknown"
#endif

class Cmdline {
struct ArgumentInfo {
char short_name;
Expand All @@ -23,6 +27,8 @@ class Cmdline {
Cmdline() {
arguments_["h"] = arguments_["help"] = [&](const std::string &) { help_requested_ = true; };
args_info_.push_back({'h', "help", "Show this help message and exit", "", false});
arguments_["v"] = arguments_["version"] = [&](const std::string &) { version_requested_ = true; };
args_info_.push_back({'v', "version", "Show version information and exit", "", false});
}
~Cmdline() {}

Expand Down Expand Up @@ -93,6 +99,11 @@ class Cmdline {
continue;
}

if (name == "v" || name == "version") {
version_requested_ = true;
continue;
}

for (i++; i < argc; i++) {
if (argv[i][0] == '-') {
i--;
Expand All @@ -111,10 +122,15 @@ class Cmdline {
help();
return false;
}
if (version_requested_) {
version();
return false;
}
return true;
}

void help() const {
std::cout << program_name_ << " " << PLOTOP_VERSION << "\n\n";
std::cout << "Usage: " << program_name_ << " [options]\n\n";
std::cout << "Options:\n";

Expand Down Expand Up @@ -143,11 +159,16 @@ class Cmdline {
}
}

void version() const {
std::cout << program_name_ << " " << PLOTOP_VERSION << "\n";
}

private:
std::unordered_map<std::string, std::function<void(const std::string &)>> arguments_;
std::vector<ArgumentInfo> args_info_;
std::string program_name_ = "plotop";
bool help_requested_ = false;
bool version_requested_ = false;
};

#endif // PLOTOP_CMDLINE_H
5 changes: 4 additions & 1 deletion makefile
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
PLATFORM ?= linux
CROSSCOMPILER ?=

# Version from git tag (same format as setuptools_scm)
VERSION := $(shell python3 scripts/get_version.py 2>/dev/null || echo "unknown")

# Compiler
CXX := $(CROSSCOMPILER)g++

# Compiler flags
CXXFLAGS := -std=c++17 -Wall
CXXFLAGS := -std=c++17 -Wall -DPLOTOP_VERSION=\"$(VERSION)\"

# Linker flags
LDFLAGS := -lstdc++fs -pthread -static-libstdc++ -static-libgcc
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[build-system]
requires = ["setuptools>=64", "setuptools_scm>=8"]
build-backend = "setuptools.build_meta"

[tool.setuptools_scm]
59 changes: 59 additions & 0 deletions scripts/get_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""Generate version string matching setuptools_scm default format."""

import re
import subprocess
import sys
from datetime import datetime, timezone


def _bump_version(tag: str) -> str:
"""Bump the last numeric segment of a version tag."""
# Strip leading 'v' or 'V'
version = tag.lstrip("vV")
parts = version.split(".")
if parts and parts[-1].isdigit():
parts[-1] = str(int(parts[-1]) + 1)
else:
parts.append("1")
return ".".join(parts)


def _git_describe() -> str:
result = subprocess.run(
["git", "describe", "--tags", "--long", "--always", "--abbrev=9"],
capture_output=True,
text=True,
)
if result.returncode != 0:
return "unknown"
return result.stdout.strip()


def _fallback_version() -> str:
desc = _git_describe()
if desc == "unknown":
return "unknown"

# Format: v0.1.3-12-g524c003
match = re.match(r"^[vV]?(.*)-(\d+)-g([0-9a-f]+)$", desc)
if not match:
return desc.lstrip("vV")

tag, distance, node = match.groups()
next_version = _bump_version(tag)
today = datetime.now(timezone.utc).strftime("%Y%m%d")
return f"{next_version}.dev{distance}+g{node}.d{today}"


def get_version() -> str:
try:
from setuptools_scm import get_version as scm_get_version

return scm_get_version()
except Exception:
return _fallback_version()


if __name__ == "__main__":
print(get_version())
6 changes: 6 additions & 0 deletions server/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from importlib.metadata import PackageNotFoundError, version

try:
__version__ = version("plotop")
except PackageNotFoundError:
__version__ = "unknown"
25 changes: 20 additions & 5 deletions server/app.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
from flask import Flask, request, jsonify, render_template
from flask_socketio import SocketIO, emit
import argparse
from datetime import datetime, timedelta
import json
import logging
import os
import queue
import socket
import threading
import time
import queue
import os
from datetime import datetime, timedelta

from flask import Flask, request, jsonify, render_template
from flask_socketio import SocketIO, emit

from server import __version__

logging.basicConfig(level=logging.INFO)

Expand Down Expand Up @@ -301,6 +305,17 @@ def handle_clear_data(message):


def main():
parser = argparse.ArgumentParser(
prog="plotop",
description="Real-time performance plotting server"
)
parser.add_argument(
"-v", "--version",
action="version",
version=f"%(prog)s {__version__}"
)
parser.parse_args()

run_socket_server()
app.logger.info("Web server starting on http://127.0.0.1:5000")
# 禁用开发服务器的警告日志
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setup(
name="plotop",
version="0.1.0",
use_scm_version=True,
packages=find_packages(include=["server", "server.*", "server.templates", "server.templates.*"]),
package_data={
"server": [
Expand Down
Loading