4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

D3.js+Node.jsでCPU使用率モニタ作成

Posted at

D3.jsの勉強がてら何か作ってみようかと思い、PCのCPU使用率を表示するものを作ってみました。

概要

実際の値を表示したかったので、
CPU使用率を返すサーバサイドをNode.jsで作成し、
情報を表示するクライアントサイドをD3.jsで作成しました。

動作例:PC3台でそれぞれサーバアプリを動作させて、クライアントで3つ分並べて表示
image.png

実施環境

  • サーバサイド
    • Node.js (v14.15.3)
      • express : 4.17.1
      • cors : 2.8.5
  • クライアントサイド
    • D3.js (v6.3.1)

※Versionに指定があるわけはなく、とりあえず試した際の各ソフトの最新Verを使用

サーバサイド(Node.js)側の実装

Node.jsにて express とcorsを使って、APIアクセスしたらCPU使用率の情報を返すアプリを作成。
CPU使用率は、os.cpus() でCPU情報を1秒周期で取得して、各モードの動作時間の増加量より算出。

サーバサイドのプロジェクト作成

プロジェクト用のディレクトリを作成したら
npmコマンドでプロジェクトを作成&必要なライブラリのインストールをする。

npm init
npm install express --save
npm install cors --save

サーバサイドのプログラム作成

実装の概要は以下

  • setInterval()を使用して1秒周期でCPU使用率を測定
  • os.cpus() で各CPUコア毎の各モードの動作時間の増加を取得
  • idle時間以外はCPU使用中とみなして、CPU使用率は 1 - (idle時間 / 全モード合計時間)で算出
    • 実装では%の値(100%の時は数値を100)にしたかったので100倍した式で記載
  • 過去100回分の測定結果を保持
  • httpでhttp://***:12345/api/performanceにアクセスしたらCPU使用率の測定履歴をJSON形式で返却
app.js
var express = require("express");
var cors = require('cors')
var os = require('os');

var app = express();
app.use(cors());
var server = app.listen(12345);

// パフォーマンス測定用
var performance = class {
    constructor() {
        this.timer = null;  // Interval Timer用
        this.info = {};     // 測定情報
    }
    // 測定開始
    start(interval, max_history) {
        // 既存タイマーの停止
        this.stop();
        // 基本の情報の作成
        let old_os = os.cpus();
        let platform = `${os.platform()} ${os.release()} (${os.arch()})`
        this.info = { 'platform': platform, 'interval': interval, 'max_history': max_history, 'items': {}};
        for(let index = 0; index < old_os.length; index++) {
            this.info.items['cpu' + index] = {'model': old_os[index].model, history:[]};
        }
        // タイマーの開始
        let info = this.info;
        this.timer = setInterval(function() {
            // 各CPU使用率を算出
            let now_os = os.cpus();
            for(let index = 0; index < now_os.length; index++) {
                // 1CPUずつCPU使用率を算出
                let total = 0;
                for (let type in now_os[index].times) { total += now_os[index].times[type]; }
                for (let type in old_os[index].times) { total -= old_os[index].times[type]; }
                let idle = now_os[index].times.idle - old_os[index].times.idle;
                let cpu_per = Math.floor(100 - (100 * idle / total));
                info.items['cpu' + index].history.unshift(cpu_per); // 先頭に最新情報追加
            }
            old_os = now_os;
            // 履歴件数超過時に古い情報削除
            for(let item in info.items) {
                while (info.items[item].history.length > max_history) { 
                    info.items[item].history.pop(); // 末尾の情報を削除
                }
            }
        }, interval);
    }
    // 測定停止
    stop() {
        if (this.timer) { 
            clearInterval(this.timer); 
            this.timer = null;
        }
    }
    // 測定履歴の取得
    get_info() {
        return this.info;
    }
};

var pref = new performance();
pref.start(1000, 100);

app.get("/api/performance", (req, res, next) => { res.json(pref.get_info()); });

サーバサイドの実行例

お試しで、ブラウザなどでhttp://***:12345/api/performanceにアクセスすると以下が返ってきます。

サーバサイドのレスポンス
{
  "platform":"linux 5.4.0-1022-raspi (arm64)",
  "interval":1000,
  "max_history":100,
  "items":{
    "cpu0":{
      "model":"unknown",
      "history":[1,0,0,0,4,5,0,0,0,1,2,2,2,0,1,0,2,5,0,1,1,0,0,1,0,2,0,41,0,0,1,0,1,2,0,6,0,5,0,1,0,1,1,1,1,1,14,23,1,0,0,0,0,1,0,0,1,2,0,41,1,0,0,2,1,0,1,1,0,2,1,1,1,0,3,1,3,7,1,2,1,1,0,0,2,1,2,0,1,1,2,9,0,0,0,0,2,0,0,2]
    },
    "cpu1":{
      "model":"unknown",
      "history":[1,1,0,0,0,0,0,1,1,2,0,1,0,0,1,1,0,3,1,1,0,0,1,1,1,2,2,11,0,3,0,1,0,0,0,1,2,1,0,2,0,0,1,1,0,0,0,0,2,0,1,0,1,1,0,1,0,0,2,7,1,1,1,0,0,6,1,0,1,1,1,1,0,3,0,1,20,17,5,3,0,0,2,1,0,1,0,1,1,0,2,10,1,1,0,1,1,1,0,0]
    },
    "cpu2":{
      "model":"unknown",
      "history":[0,0,0,2,2,0,0,1,3,1,0,2,1,1,1,1,0,2,1,2,1,2,1,2,2,0,2,40,1,4,1,0,1,2,1,1,1,1,1,0,0,1,0,1,0,1,3,2,3,4,0,2,1,2,1,1,1,1,0,43,1,0,1,0,0,1,1,0,0,0,0,0,1,0,0,1,0,1,2,2,1,2,1,0,0,1,1,0,2,0,1,46,1,2,0,5,0,0,1,0]
    },
    "cpu3":{
      "model":"unknown",
      "history":[1,1,2,2,2,3,1,0,2,1,1,1,1,1,0,1,8,20,1,2,0,1,0,0,0,1,2,9,1,4,1,1,0,0,3,0,2,0,1,0,3,1,0,1,2,0,1,2,28,12,1,3,1,0,2,0,0,0,2,8,0,1,0,1,2,0,0,0,1,2,1,1,0,0,1,1,1,1,0,5,1,1,1,0,1,2,1,2,0,2,0,34,1,1,1,0,1,2,0,2]
    }
}}

クライアントサイドの作成

D3.jsでCPU使用率をグラフ表示する処理を作成

実装の概要は以下

  • performanceMonitorクラス
    • 1PCのCPU使用率情報を表示するためのクラス
    • コンストラクタ
      • D3.jsを使って、ベースとなるSVGの追加
      • 座標計算用のd3.scaleLinearや、折れ線グラフ用のd3.lineの準備
      • グラフの背景や、Y軸目盛の作成
    • updateメソッド
      • fetch APIを使って、サーバサイドのCPU使用率情報を取得
      • CPU使用率情報の取得に成功したら、drawメソッドを呼び出して描画更新させる
    • drawメソッド
      • 初回のみSVGに折れ線グラフ表示用のpathと、凡例用のlineとtextを追加
      • CPU使用率情報に従ってpath(折れ線グラフ)を更新、凡例のtext内容を更新
  • setInterval()関数を使って1秒ごとにCPU情報を再取得して描画更新
monitor.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>Performance Monitor</title>
        <script type="text/javascript" src="https://d3js.org/d3.v6.min.js"></script> 
    </head>
    <body>
        <div id="monitor1" style="float:left;"></div>
        <div id="monitor2" style="float:left;"></div>
        <div id="monitor3" style="float:left;"></div>
        <script type="text/javascript">
            class performanceMonitor {
                constructor(select, width, height, url) {
                    const margin = {'top': 10, 'bottom':10, 'left':40, 'right': 40};
                    const history_max = 100;
                    this.width = width;
                    this.height = height;
                    this.url = url;
                    this.items = {};
                    this.svg = d3.select(select).append("svg").attr("width", width).attr("height", height);
                    this.yScale = d3.scaleLinear().domain([0, 100]).range([height - margin.bottom, margin.top]);
                    this.xScale = d3.scaleLinear().domain([0, history_max]).range([width - margin.right, margin.left]);
                    this.cScale = d3.scaleOrdinal(d3.schemePaired);
                    this.line = d3.line().x((d, i) => this.xScale(i)).y((d, i) => this.yScale(d));
                    // グラフの背景
                    this.svg.append("rect").attr("fill","#F8F8F8").attr("stroke","#000000")
                        .attr("x", this.xScale(100)).attr("width", this.xScale(0)-this.xScale(100))
                        .attr("y", this.yScale(100)).attr("height", this.yScale(0)-this.yScale(100));
                    // 左右にY軸表示
                    this.svg.append("g").attr("class", "axis axis-l").attr("transform", `translate(${margin.left},0)`)
                        .call(d3.axisLeft(this.yScale).ticks(10));
                    this.svg.append("g").attr("class", "axis axis-r").attr("transform", `translate(${width - margin.right},0)`)
                        .call(d3.axisRight(this.yScale).ticks(10));
                    // プラットフォームの表示
                    this.platform = this.svg.append("text").attr("x",65).attr("y",40).attr("font-size","16px");
                }
                update() {
                    let parent = this;
                    fetch(this.url).then(res => res.json()).then(data => parent.draw(data));
                }
                draw(data) {
                    // プラットフォームテキストの更新
                    this.platform.text(data.platform);
                    // アイテム数分繰り返し
                    for(let item in data.items) {
                        if ((item in this.items) == false) {
                            // 初回のみsvgにpathを追加
                            let index = Object.keys(this.items).length;
                            let offset_y = 60 + 16 * index;
                            this.items[item] = {};
                            this.items[item].color = this.cScale(index);
                            this.items[item].path = this.svg.append("path");                            
                            this.items[item].line = this.svg.append("line")
                                .attr("x1",65).attr("x2",75).attr("y1",offset_y).attr("y2",offset_y)
                                .attr("stroke-width",2).attr("stroke",this.items[item].color);
                            this.items[item].text = this.svg.append("text").
                                attr("x",80).attr("y",offset_y).attr("font-size","10px");
                        }
                        // データの更新
                        this.items[item].path.datum(data.items[item].history)
                            .attr("fill", "none")
                            .attr("stroke", this.items[item].color)
                            .attr("d", this.line);
                        this.items[item].text.text(`${item} [${data.items[item].model}]  ${data.items[item].history[0]}%`);
                    }
                }
            }
            var monitor1 = new performanceMonitor("#monitor1", 600, 400, "http://192.168.11.12:12345/api/performance");
            var monitor2 = new performanceMonitor("#monitor2", 600, 400, "http://192.168.11.5:12345/api/performance");
            var monitor3 = new performanceMonitor("#monitor3", 600, 400, "http://192.168.11.11:12345/api/performance");

            function updateDataset() {
                monitor1.update();
                monitor2.update();
                monitor3.update();
            }
            setInterval(updateDataset,1000);
            updateDataset();
        </script>
    </body>
</html>
  • performanceMonitorのインスタンス作成部分 monitor1monitor2monitor3のところは、各環境に合わせて変更を
    • 表示件数を増減させたければ、 <div id="monitor1">のところと、new performanceMonitorしているところと、monitor1.update();を呼び出しているところの3か所を修正

以上

サーバサイドをNode.jsで実装し、CPU使用率などもNode.jsの標準のもので取得しているため、
動作させるPCなどを意識せずに利用できます。
⇒実際にWindows10と、Ubuntu(RasPi3)で同じサーバアプリを動かしてみてます。

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?