Stimulator

機械学習とか好きな技術話とかエンジニア的な話とかを書く

Xonshでmatplotlibグラフをコンソールにインライン描画してメモリ状況を観察する

- はじめに -

この記事はXonsh Advent Calendar 2017 4日目の記事です。

Xonshの中にはxontribというメソッド群が存在します。
その中のmplhooksは、画像の描画をサポートしてくれます。

本記事では以下について記述します

  • xontrib.mplhooksを利用したxonshコマンドプロンプト上へのMatplotlibグラフのインライン描画
  • vm_statコマンドを利用したMac OS Xのメモリ状況の取得とPythonによるparse
  • Xonshでメモリ状況のリアルタイム可視化

以下アジェンダです


 

- Xonshのグラフインライン描画 -

Xonshでグラフは端末状にインライン描画する方法は、以下の2種類が容易されています。

  • xontrib.mplhooks.display_figure_with_iterm2(fig)
  • xontrib.mplhooks.show()

以下がソースコードです。
xontrib.mplhooks — xonsh 0.5.12.dev97 documentation
xonsh/mplhooks.py at master · xonsh/xonsh · GitHub
Pythonなのですぐ読めます。

display_figure_with_iterm2は、 iterm2_toolsを使ってiTerm2上にグラフを描画するようになっています。
showは、iterm2_toolsがimportできればdisplay_figure_with_iterm2で描画、iterm2_toolsが存在しない場合はplt.gcf()で現状プロット図を取得してきてRGB値をcolor stringにして描画する形になっています。

iTerm2でインライン描画

私はiTerm2利用者ですので以下でiterm2_toolsを導入しています。

pip install iterm2_tools

GitHub - asmeurer/iterm2-tools: iTerm2 tools for Python

以下エラーが出るのでXonshコンソールを再起動しておきます。

NameError: name 'display_image_bytes' is not defined

再起動後、Xonshコンソール上で以下を淡々と入力していきます。
もしくは適当にxshスクリプトにしてもOKです。

import matplotlib.pyplot as plt
import xontrib.mplhooks
import numpy as np
x = np.linspace(-3, 3, 20)
y = x ** 2
fig = plt.figure(figsize=(6, 4))
ax1 = fig.add_subplot(1, 1, 1)
ax1.plot(x, y)
xontrib.mplhooks.display_figure_with_iterm2(fig)

f:id:vaaaaaanquish:20171121140615p:plain:w350
xonsh上でもインライン描画できました

 

Terminal.app上のxonshでインライン描画

試しにMacにデフォルトで付随するTerminal.appでもXonsh上でインライン描画をしてみます。

上記iTerm2の時と同じGraphを作って以下。

xontrib.mplhooks.show()

f:id:vaaaaaanquish:20171121141037p:plain:w350
ちょっとアレですがまあまあ…

iTerm2の描画は良いですね

 

itermplotでインライン描画

上記2つに加えてiTerm2にはインライン描画パッケージとして、itermplotというのがあります。
GitHub - daleroberts/itermplot: An awesome iTerm2 backend for Matplotlib, so you can plot directly in your terminal.

Animation supportもされており、インラインでリアルタイム表示ができそうです。
(といっても事前にGIF画像にしてiTermの表示機能に送る形ですが…)

導入はpipで、xonshを起動する前にMPLBACKENDにitermplotを指定しておくとplt.show()がインライン描画になります。

pip install itermplot
export MPLBACKEND="module://itermplot"

また、後述しますがリアルタイム表示するにはImageMagickが必要なのでインストールしておきます。

brew install imagemagick

sampleスクリプトを動かしてみます。

import matplotlib.pyplot as plt
plt.plot([1,2,3])
plt.show()

f:id:vaaaaaanquish:20171202214602p:plain:w350
いちばんつよそう

背景が黒い場合は以下で良さげな表示に

export ITERMPLOT=rv


  

- vm_statコマンドでメモリ可視化 -

Macでメモリ状況を出力するコマンドではfreeやtop、vm_statといった選択肢があります。

今回は適当にvm_statを選択します。
Xonshであれば以下コマンドどちらも結果を得ることができます

vm_stat
$(vm_stat)    # python string

vm_statコマンドの結果をparseしてdictにする関数を作って実行してみます。

def vms():
    vm_dic = {}
    for i,x in enumerate($(vm_stat).split("\n")):
        row = x.split(":")
        if i>1 and len(row)==2:
            vm_dic[row[0]] = int(row[1].strip().replace(".",""))*4096/(1024.0 ** 3)
    return vm_dic

以下のようにGB単位でメモリ状況が出力されます。

$ vms()
{'"Translation faults"': 2588.0195083618164,
 'Anonymous pages': 7.266414642333984,
 'Compressions': 54.13356018066406,
 'Decompressions': 33.81982421875,
 'File-backed pages': 2.6624984741210938,
 'Pageins': 32.48748779296875,
 'Pageouts': 0.26355743408203125,
 'Pages active': 4.984306335449219,
 'Pages copy-on-write': 82.98124694824219,
 'Pages inactive': 4.944099426269531,
 'Pages occupied by compressor': 3.5574417114257812,
 'Pages purgeable': 0.14667129516601562,
 'Pages purged': 1.3369598388671875,
 'Pages reactivated': 22.909423828125,
 'Pages speculative': 0.000507354736328125,
 'Pages stored in compressor': 8.809425354003906,
 'Pages throttled': 0.0,
 'Pages wired down': 2.446277618408203,
 'Pages zero filled': 1392.7644996643066,
 'Swapins': 16.50379180908203,
 'Swapouts': 17.286388397216797}

 
せっかくですのでwatchコマンドのように、メモリ状況をリアルタイムで表示し続けてみます。

import json
while True:
    vm_dic = vms()
    $[clear]
    print(json.dumps(vm_dic, indent=4))

f:id:vaaaaaanquish:20171121144149g:plain:w400

watch vm_statでよくない!?みたいな何かが出来上がりました。

vms functionをprintするスクリプトにして、watchもできます。
watchコマンドはデフォルトでsh -cにコマンドを飛ばすだけなので、--exec, -xで指定できるよう環境設定するか、以下のようにxonshを定期的に作るという酷いやつで対応できます。

# mac -> $ brew install watch
watch "xonsh sample.xsh"

watch vm_statでよくない!?みたいな何かです。

  

- vm_statをMatplotlibでリアルタイム描画 -

上記2項目で作った関数を利用して、Matplotlibでvm_statの結果をリアルタイム描画してみます。

wired, active, inactive, speculative辺りがusedメモリに関連する項目ですのでそちらを可視化してみます。
(occupied by compressorも必要かな?)

スクリプトが適当です。

import time
import xontrib.mplhooks
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('ggplot') 

def init_data(fig, id, title):
    x = np.zeros(100)
    y = np.zeros(100)
    subplot = fig.add_subplot(2,2,id)
    subplot.set_title(title)
    subplot.set_ylabel('GB')
    subplot.set_xlabel('time')
    li, = subplot.plot(x, y)
    return x, y, subplot, li

def set_data(x,y,vm_dic,key):
    x = np.append(x, time.time())
    x = np.delete(x, 0)
    y = np.append(y, vm_dic[key])
    y = np.delete(y, 0)
    return x,y

def update_fig(x, y, subplot, li):
    li.set_xdata(x)
    li.set_ydata(y)
    subplot.set_ylim(min(y), max(y))
    subplot.set_xlim(min(x), max(x))

def watch_vms_windows():
    fig = plt.figure()
    x,y,subplot,li = init_data(fig, 1, "Pages active")
    x2,y2,subplot2,li2 = init_data(fig, 2, "Pages inactive")
    x3,y3,subplot3,li3 = init_data(fig, 3, "Pages speculative")
    x4,y4,subplot4,li4 = init_data(fig, 4, "Pages wired down")
    count = 0
    while True:
        vm_dic = vms()
        x,y = set_data(x, y, vm_dic, "Pages active")
        x2,y2 = set_data(x2, y2, vm_dic, "Pages inactive")
        x3,y3 = set_data(x3, y3, vm_dic, "Pages speculative")
        x4,y4 = set_data(x4, y4, vm_dic, "Pages wired down")
        if count > 100:
            update_fig(x, y, subplot, li)
            update_fig(x2, y2, subplot2, li2)
            update_fig(x3, y3, subplot3, li3)
            update_fig(x4, y4, subplot4, li4)
            plt.pause(.01)
        else:
            count += 1
            print(count, end="\r")

以下参考にしています。
Arduino で測定したデータを Matplotlib でリアルタイムプロット | org-技術

100ループ回してGraph描画に十分なデータを集めたらGraph描画を開始するスクリプトです。

f:id:vaaaaaanquish:20171121144909g:plain

QuickTime Playerを利用して画面の動画を撮っているのでactiveがガンガン上昇しinactiveが下降、メモリ利用と廃棄が頻繁に行われている事がわかります。


$[clear] して display_figure_with_iterm2(fig) でインライン描画するスクリプトにする事も可能で、試しましたがdisplay_figure_with_iterm2では実行時描画になるため、思ったより綺麗に表示されませんでした。


 

- MatplotlibのAnimationモジュールを使ってリアルタイム描画を実現する -

Matplotlibにはpltで作られた配列をアニメーションにして表示するモジュールが存在する。
全てのGraphを事前に配列に取っておいて描画するArtistAnimation、動的に生成していくFuncAnimationの2つである。
animation module — Matplotlib 2.1.0 documentation

Animationはgifやmp4を生成するだけなので、これらを前述したitermplotに流してiTerm2にインライン描画する。

itermplotのカッケー背景はAnimationには対応してないので以下だけ設定しておく。

export MPLBACKEND="module://itermplot"
export ITERMPLOT_FRAMES=100

適当に書いたコード

import time
import xontrib.mplhooks
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
plt.style.use('ggplot') 

class VMS:
    def __init__(self, fig):
        self.fig = fig
        self.x, self.y, self.subplot, self.li = self.init_data(1, "Pages active")
        self.x2, self.y2, self.subplot2, self.li2 = self.init_data(2, "Pages inactive")
        self.x3, self.y3, self.subplot3, self.li3 = self.init_data(3, "Pages speculative")
        self.x4, self.y4, self.subplot4, self.li4 = self.init_data(4, "Pages wired down")
        self.c = 0
        for i in range(100):
            self.plotsub(i)
        
    def init_data(self, id, title):
        x = np.zeros(100)
        y = np.zeros(100)
        subplot = self.fig.add_subplot(2,2,id)
        subplot.set_title(title)
        subplot.set_ylabel('GB')
        li, = subplot.plot(x, y)
        return x, y, subplot, li

    def vms(self):
        vm_dic = {}
        for i,x in enumerate($(vm_stat).split("\n")):
            row = x.split(":")
            if i>1 and len(row)==2:
                vm_dic[row[0]] = int(row[1].strip().replace(".",""))*4096/(1024.0 ** 3)
        return vm_dic

    def count(self):
        self.c += 1

    def plotsub(self, frame):
        vm_dic = self.vms()
        self.count()
        # subplot1
        self.x = np.append(self.x, self.c)
        self.x = np.delete(self.x, 0)
        self.y = np.append(self.y, vm_dic["Pages active"])
        self.y = np.delete(self.y, 0)
        self.li.set_xdata(self.x)
        self.li.set_ydata(self.y)
        self.subplot.set_ylim(min(self.y), max(self.y))
        self.subplot.set_xlim(min(self.x), max(self.x))
        self.subplot.set_xticks(np.arange(min(self.x), max(self.x), 20))
        # subplot2
        self.x2 = np.append(self.x2, self.c)
        self.x2 = np.delete(self.x2, 0)
        self.y2 = np.append(self.y2, vm_dic["Pages inactive"])
        self.y2 = np.delete(self.y2, 0)
        self.li2.set_xdata(self.x2)
        self.li2.set_ydata(self.y2)
        self.subplot2.set_ylim(min(self.y2), max(self.y2))
        self.subplot2.set_xlim(min(self.x2), max(self.x2))
        self.subplot2.set_xticks( np.arange(min(self.x2), max(self.x2), 20) )
        # subplot3
        self.x3 = np.append(self.x3, self.c)
        self.x3 = np.delete(self.x3, 0)
        self.y3 = np.append(self.y3, vm_dic["Pages speculative"])
        self.y3 = np.delete(self.y3, 0)
        self.li3.set_xdata(self.x3)
        self.li3.set_ydata(self.y3)
        self.subplot3.set_ylim(min(self.y3), max(self.y3))
        self.subplot3.set_xlim(min(self.x3), max(self.x3))
        self.subplot3.set_xticks( np.arange(min(self.x3), max(self.x3), 20) )
        # subplot4
        self.x4 = np.append(self.x4, self.c)
        self.x4 = np.delete(self.x4, 0)
        self.y4 = np.append(self.y4, vm_dic["Pages wired down"])
        self.y4 = np.delete(self.y4, 0)
        self.li4.set_xdata(self.x4)
        self.li4.set_ydata(self.y4)
        self.subplot4.set_ylim(min(self.y4), max(self.y4))
        self.subplot4.set_xlim(min(self.x4), max(self.x4))
        self.subplot4.set_xticks( np.arange(min(self.x4), max(self.x4), 20) )


fig = plt.figure(figsize=(8,8))
vms = VMS(fig)
animation.FuncAnimation(vms.fig, vms.plotsub, blit=True)
plt.show()

plt.show()がitermplotによって継承されてればインラインで見れる
f:id:vaaaaaanquish:20171203221702g:plain

これは動的に見れている訳ではなく、事前に100フレーム分をgif画像にしてbyteIOでiTerm2に送り込んでいるだけなのでリアルタイムの本質を見失っている状態である。

iTerm2に表示できるGIFのフレーム数を上手くコントロールできないとか課題は多いがまあできたという事で…

 

- おわりに -

「これ watch vm_stat で良くね!?」
「リアルタイムインライン描画とは」
Pythonスクリプトなら os.system や commands.getoutput でコマンド実行できるよね!?」
「これXonshじゃなくても良くない!?」
等の耳が痛い声が聞こえて来ます。

どんどんおかしい方向に進んだ気がしますが、xonshの繁栄のための知見として納めておきます。

おわり。