Python tkinter：CPU使用率グラフを作る（１）

(2025.01.04) コード修正

## はじめに
tkinter を使って CPU 使用率グラフを作ってみた、
Mac 標準装備のアクティビティモニタでもいいのだが、欲しい情報を１つの画面に表示したくて今回のものを作った次第である。
こういうものでモニタリングしていると、かなりハードな計算でも４コア程度しか使われておらず、８コア全て使い切るには並列処理などを意識して使っていく必要があることがよくわかる。
環境は以下の通り。

MacBook Pro 14"
Chip Apple M1 Pro (8-core CPU, 14-core GPU)
Memory 16GB
Storage 512GB SSD
macOS Sequoia 15.2
Python 3.13.1
Tcl/Tk 9.0.1


## 作例
まずは作図事例を示そう。
１枚目の写真はアイドリング時のもの。２枚目の写真は、Python で並列処理をかけて全 CPU を動かした場合のもの。
私が使っている M1 MacBook Pro は 8 コア CPU なので、CPU 稼動率の履歴を示すグラフは８段としている。グラフの上２段が高効率コア、下６段が高性能コア。並びは Mac 標準のアクティビティモニタの表示（CPU history）から類推。これでいいと思う。

![IMG_2279.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129300/874fe014-8084-b86b-3e30-5e532273bd6b.jpeg)

![IMG_2281.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129300/1dffb4d5-d34d-9602-abe1-6c075609bf20.jpeg)


## 概略説明
tkinter の画面全体を２つの部分（Frame）に分けている。
上段 Frame には８個の Canvas を配置、下段 Frame には平均 CPU 稼動率とメモリ使用率を表示するための４個の Label を配置している。

グラフの中には、各 CPU 毎に、コアの番号・最新の CPU 稼動率の数値・CPU 稼動率の履歴グラフを表示している。履歴の横軸は６０秒であり１秒ステップで稼動率の値を取得・更新している。

グラフの表示方法については、ネットで検索すると matplotlib で描画しこれを Canvas に貼り付けるものが多くみられるが、ここでは複雑な情報を表示するものでもないので、Canvas に直接描画している。
また、折れ線表示だと視覚的に認識しづらいので、グラフ更新ステップである１秒相当の幅の線を create_line で棒グラフ状に描画している。

グラフの時間ステップでの更新は、x軸として固定の numpy 配列 xx を準備（０から６０までの６１個の要素）、y軸として CPU 稼動率格納用に６１要素の numpy 配列を準備、y軸の数値を１秒毎にずらしながら ```0.5*(xx[i]+xx[i+1])``` 及び ```0.5*(yy[i]+yy[i+1])``` の位置に棒線を描画していく。この際、線には ```line```、稼動率数値には ```sval``` というタグをつけ、グラフ更新直前に ```delete('line')```, ```delete('sval')``` している。


## コード
```python
import tkinter as tk
from tkinter import ttk
import numpy as np
import psutil


def update():
    global root,cv,xx,yy,mlb,ww,hh,xmin,xmax,ymin,ymax,cpu,fontn
    kxi,kxf,kyi,kyf=0,ww,0,hh
    lw=int((kxf-kxi)/(xmax-xmin))+1 # line width
    r=psutil.cpu_percent(interval=1, percpu=True)
    work=yy
    yy=np.roll(work,-1,axis=1)
    yy[:,-1]=np.array(r)
    for k in range(0,len(r)):
        cv[k].delete('line')
        cv[k].delete('sval')
        for i in range(0,len(xx)-1):
            xx2=0.5*(xx[i]+xx[i+1])
            yy2=0.5*(yy[k,i]+yy[k,i+1])
            xx1,yy1=xx2,ymin
            x1=kxi+(xx1-xmin)*(kxf-kxi)/(xmax-xmin)
            y1=kyf-(yy1-ymin)*(kyf-kyi)/(ymax-ymin)
            x2=kxi+(xx2-xmin)*(kxf-kxi)/(xmax-xmin)
            y2=kyf-(yy2-ymin)*(kyf-kyi)/(ymax-ymin)
            cv[k].create_line(x1,y1,x2,y2,width=lw,fill='#999999',tag='line')
        s='{0:3s} {1:6.1f}'.format(cpu[k],r[k])
        cv[k].create_text(5,5,text=s,anchor='nw',font=(fontn),fill='#ffff00',tag='sval')
    mlb[1]['text']='{0:6.1f}%'.format(sum(r)/len(r))    
    mlb[3]['text']='{0:6.1f}%'.format(psutil.virtual_memory().percent)
    root.after(1000, update)


def main():
    global root,cv,xx,yy,mlb,ww,hh,xmin,xmax,ymin,ymax,cpu,fontn

   # basin parameter
    nc=psutil.cpu_count() # number pf cores
    cpu=[]
    for i in range(0,nc):
        s='P'  # performance core
        if 0<=i<=1: s='E' # efficiency core
        cpu.append('{0}-{1}'.format(i+1,s))
    ww,hh=200,50 # size of canvas
    tspan=60 # time span of fig
    tstr='CPU (ts={0:.0f}s)'.format(tspan)
    # initialization of plot data
    xmin,xmax,dx=0,tspan,1 # time range 
    ymin,ymax=0,100 # CPU percentage
    xx=np.linspace(xmin,xmax,int((xmax-xmin)/dx)+1)
    yy=np.zeros((nc,len(xx)),dtype=np.float64)

    # GUI
    root =tk.Tk()
    root.resizable(False,False)
    root.title(tstr)
    style=ttk.Style()
    style.theme_use('alt')
    fontn='Calibri 12 bold'
    fontb='Calibri 14 bold'
    style.configure('.',font=(fontn))
    style.configure('.',background='#393939',foreground='#ffffff')

    # arrangement of Canvas
    frame0=ttk.Frame(root)
    cv=np.empty(nc,dtype=object)
    for k in range(0,len(cv)):
        cv[k]=tk.Canvas(root,width=ww,height=hh,bg='#0f0f0f')
        cv[k].pack()
    frame0.pack()

    # arrangement of Lavel
    frame1=ttk.Frame(root)
    nn=4
    mlb=np.empty(nn,dtype=object)
    txt=['CPU','','MEM','']
    for k in range(0,len(mlb)):
        mlb[k]=ttk.Label(frame1,text=txt[k],borderwidth=1,relief=tk.SOLID,anchor=tk.CENTER,width=7,font=(fontb))
        i,j=k//2,k%2
        mlb[k].grid(row=i,column=j)
    frame1.pack()

    update()

    root.mainloop()


if __name__ == '__main__': main()
```

以　上