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


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

## 概要

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

動作例：PC3台でそれぞれサーバアプリを動作させて、クライアントで3つ分並べて表示
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538393/0c7203c0-aa47-e60e-6fa9-5ce7d9785985.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()](https://nodejs.org/api/os.html#os_os_cpus) でCPU情報を1秒周期で取得して、各モードの動作時間の増加量より算出。

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

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

```bash
npm init
npm install express --save
npm install cors --save
```
### サーバサイドのプログラム作成

実装の概要は以下
* setInterval()を使用して1秒周期でCPU使用率を測定
* [os.cpus()](https://nodejs.org/api/os.html#os_os_cpus) で各CPUコア毎の各モードの動作時間の増加を取得
* idle時間以外はCPU使用中とみなして、CPU使用率は `1 - (idle時間 / 全モード合計時間)`で算出
    * 実装では%の値（100%の時は数値を`100`)にしたかったので100倍した式で記載
*  過去100回分の測定結果を保持
*  httpで`http://***:12345/api/performance`にアクセスしたらCPU使用率の測定履歴をJSON形式で返却

```js: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`にアクセスすると以下が返ってきます。

```json:サーバサイドのレスポンス
{
  "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情報を再取得して描画更新


```html: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のインスタンス作成部分 `monitor1`、`monitor2`、`monitor3`のところは、各環境に合わせて変更を
    * 表示件数を増減させたければ、 `<div id="monitor1">`のところと、`new performanceMonitor`しているところと、`monitor1.update();`を呼び出しているところの3か所を修正


## 以上

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