メモリを割とガッツリ解放するAppleScriptを作る過程

前回、メモリを解放する効果のあるコマンドを覚えた。

  • du -sx / >& /dev/null & sleep 5 && kill $!
  • diskutil repairPermissions /
  • purge

ターミナルを開いて、これら3つのコマンドを実行すると、予想以上に気持ちよくメモリを解放してくれた。これならメモリ不足を感じたら実行する定例の操作として、覚えておく価値は十分ありそう。しかし、毎回ターミナルから3つを実行するのは、コマンド履歴を利用したとしても、だんだん面倒になってきた。ではどうするか?いつものAppleScriptでやってみることにした。

基本

  • AppleScriptからコマンドを実行するのは簡単。do shell script "コマンド" を使うだけ。
  • よって、メインの処理自体は以下のように書ける。シンプル。


do shell script "purge"
do shell script "diskutil repairpermissions /"
do shell script "du -sx / >& /dev/null & sleep 5 && kill $!"

  • purgeから実行するのは、purge実行中の十数秒間だけ、すべての操作を受け付けなくなってしまうため。
    • スクリプト実行直後の意識した十数秒間は我慢できるけど、
    • いつになるか分からない最後の十数秒間は我慢できない、と思ったので。

定期的に実行する

  • 上記の3行でも目的は十分達成されるのだが、相変わらず毎回実行する操作は面倒なまま。
  • 定期的に自動実行するように修正してみた。


on idle {} my main() return 3600 --1時間毎に実行する
end idle

on main() do shell script "purge"
do shell script "diskutil repairpermissions /"
do shell script "du -sx / >& /dev/null & sleep 5 && kill $!"
end main

  • 上記コードを以下の形式で保存した。
    • ファイルフォーマット:アプリケーション
    • オプション:「実行後、自動的に終了しない」チェックあり
  • これで1時間(=3600秒)毎に、定期的に実行されるようになった。

メモリを監視する

  • しかし、1時間ごとの定期実行だと、その前にメモリ不足に陥る可能性もあるかも。
  • あるいは十分な空き領域があるにもかかわらず、無駄に実行するかもしれない。
  • ベストは、メモリの空き領域を監視して、必要な時に実行するべきである。
メモリの空き領域を取得する
  • AppleScriptにメモリの空き領域を取得する関数はない。
  • しかし、vm_statコマンドを実行すれば、欲しい情報も含めて取得できる。


do shell script "vm_stat"

  • 戻り値は、ターミナルでvm_statを実行したのと同じ結果がテキストで返ってくる。
  • しかし、テキストの羅列から任意の1項目の値を得るのは骨が折れる。
  • だから、以下のような方法でレコード(連想配列のようなもの)に変換してしまう。


on vm_stat() "vm_stat|head -n12|tail -n11|sed 's/Pages/pages_/'|tr -d ' .\"-'|tr '\n\r' ','|sed 's/,$//'"
run script "{" & (do shell script result) & "}"
end vm_stat

  • vm_statで始まるワンライナーを適当に訳すと、以下のような意味になる。
    • vm_stat|先頭から12行|末尾から11行|"Pages "を"pages_"に変換|スペースと.と"を削除|改行コードを,カンマに変換|末尾の,カンマだけ削除
  • つまり、上記のワンライナーからは以下のようなテキストが返ってくる。
pages_free:210207,pages_active:592236,pages_inactive:107849,pages_speculative:14702,pages_wireddown:122792,Translationfaults:240516233,pages_copyonwrite:6330983,pages_zerofilled:84303808,pages_reactivated:793921,Pageins:1529592,Pageouts:217373
  • 上記テキストを{}で囲って、run script(evalのようなもの)で実行すれば、レコードオブジェクトになるのだ。
MB単位で取得する
  • 上記で取得したレコードのpages_freeとpages_speculativeを合算すると、空き領域の容量となる。
  • ところで、vm_statで返される値は page(=4096バイト)単位。見慣れたMB単位に変換するには、
  • 4096÷1024÷1024 = 1/256。つまり、256で割るとMB単位に変換できるのだ。


on idle {} set vm to my vm_stat() if ( (vm's pages_free) + (vm's pages_speculative) ) / 256 < 500 then
my main() end if
return 600 --10分毎に実行する
end idle

on main() do shell script "purge"
do shell script "diskutil repairpermissions /"
do shell script "du -sx / >& /dev/null & sleep 5 && kill $!"
end main

on vm_stat() "vm_stat|head -n12|tail -n11|sed 's/Pages/pages_/'|tr -d ' .\"-'|tr '\n\r' ','|sed 's/,$//'"
run script "{" & (do shell script result) & "}"
end vm_stat

  • これで、10分毎にメモリを監視して、空き領域が500MB未満になっていたら、メモリ解放の処理を実行する。

起動中にDockアイコンをクリックした時の処理

  • reopen()ハンドラは、起動中に再度開く操作をした時に実行される。
  • 具体的には、Dockアイコンをクリックしたり、Finderからアイコンをダブルクリックして、再度開く操作をした時である。


on idle {} set vm to my vm_stat() if ( (vm's pages_free) + (vm's pages_speculative) ) / 256 < 500 then
my main() end if
return 600 --10分毎に実行する
end idle

on reopen {} my main() end reopen

on main() do shell script "purge"
do shell script "diskutil repairpermissions /"
do shell script "du -sx / >& /dev/null & sleep 5 && kill $!"
end main

on vm_stat() "vm_stat|head -n12|tail -n11|sed 's/Pages/pages_/'|tr -d ' .\"-'|tr '\n\r' ','|sed 's/,$//'"
run script "{" & (do shell script result) & "}"
end vm_stat

  • このようにreopen()ハンドラ内で無条件にmain()を実行するようにしておくことで、
  • 時間や空き容量に関係なく、メモリ解放の処理を好きなタイミングで実施する手段を提供できるのだ。

処理前に確認する

  • 自動実行になって、一つ気に入らない点。purgeが実行されて、突然10秒くらい反応しなくなってしまうこと。
    • メモリ解放の処理前に、警告することにした。
  • また、間違ってDockアイコンを触ってしまったり、定期的な処理を実行したくない場合もあるかもしれない。
    • キャンセルできるようにした。


on idle
set vm to my vm_stat() if ( (vm's pages_free) + (vm's pages_speculative) ) / 256 < 500 then
my main() end if
return 600 --10分毎に実行する
end idle

on reopen
my main() end reopen

on main() activate
"メモリ解放のため、以下のコマンドを実行します。\n(最初の15秒間だけ、一時停止します。)\n\n purge\n diskutil repairPermissions /\n du -sx /"
display dialog result --with icon caution --giving up after 4
do shell script "purge"
do shell script "diskutil repairpermissions /"
do shell script "du -sx / >& /dev/null & sleep 5 && kill $!"
end main

on vm_stat() "vm_stat|head -n12|tail -n11|sed 's/Pages/pages_/'|tr -d ' .\"-'|tr '\n\r' ','|sed 's/,$//'"
run script "{" & (do shell script result) & "}"
end vm_stat

処理状況を通知する

  • ここまでの処理、空き状況をみてメモリ解放の処理を実施するが、フィードバックが何もない。
  • 本当にちゃんと機能しているのか?不安に駆られる。開発中などは特に。
  • そこで、growlnotifyなどを利用して、処理状況を通知するようにしてみた。


on idle
set vm to my vm_stat() if ( (vm's pages_free) + (vm's pages_speculative) ) / 256 < 500 then
my main() end if
return 600 --10分毎に実行する
end idle

on reopen
my main() end reopen

on main() activate
"メモリ解放のため、以下のコマンドを実行します。\n(最初の15秒間だけ、一時停止します。)\n\n purge\n diskutil repairPermissions /\n du -sx /"
display dialog result --with icon caution --giving up after 4
my message("purge", "操作を受け付けない状態になります。\n15秒ほどお待ちください。") do shell script "purge"
my message("diskutil repairpermissions /", "3分ほどバックグラウンドで処理します。") do shell script "diskutil repairpermissions /"
my message("du -sx /", "あと5秒で完了します。") do shell script "du -sx / >& /dev/null & sleep 5 && kill $!"
end main

on vm_stat() "vm_stat|head -n12|tail -n11|sed 's/Pages/pages_/'|tr -d ' .\"-'|tr '\n\r' ','|sed 's/,$//'"
run script "{" & (do shell script result) & "}"
end vm_stat

on message(title, msg) try
do shell script "/usr/local/bin/growlnotify_ -m " & quoted form of msg & space & quoted form of title & " 2>&1"
if result is not "" then error -128
on error
activate
display alert title message msg giving up after 4
end try
end message

  • message()ハンドラは、growlnotifyコマンドがインストールされていればgrowlで通知し、なければAppleScriptのdisplay alertで通知する。

処理の効果を表示する

  • 果たして、どれほどメモリ解放の効果があったのか?とっても気になる。


on idle
set vm to my vm_stat() if ( (vm's pages_free) + (vm's pages_speculative) ) / 256 < 500 then
my main() end if
return 600 --10分毎に実行する
end idle

on reopen
my main() end reopen

on main() activate
"メモリ解放のため、以下のコマンドを実行します。\n(最初の15秒間だけ、一時停止します。)\n\n purge\n diskutil repairPermissions /\n du -sx /"
display dialog result --with icon caution --giving up after 4
set msg to {} set msg's end to my datetime() set msg's end to my memory_info(my vm_stat()) my message("purge", "操作を受け付けない状態になります。\n15秒ほどお待ちください。") do shell script "purge"
set msg's end to my memory_info(my vm_stat()) my message("diskutil repairpermissions /", "3分ほどバックグラウンドで処理します。") do shell script "diskutil repairpermissions /"
set msg's end to my memory_info(my vm_stat()) my message("du -sx /", "あと5秒で完了します。") do shell script "du -sx / >& /dev/null & sleep 5 && kill $!"
set msg's end to my memory_info(my vm_stat()) set msg's end to my datetime() activate
display alert "メモリ状態の遷移" message my join(msg, return) end main

on datetime() do shell script "date '+ at %Y-%m-%d %H:%M:%S'"
end datetime

on memory_info(r) "Free: " & my MB_GB( (r's pages_free) + (r's pages_speculative) ) & ¬ " Inactive: " & my MB_GB(r's pages_inactive) & ¬ " Active: " & my MB_GB(r's pages_active) & ¬ " Wired: " & my MB_GB(r's pages_wireddown) end memory_info

--1 page = 4096B
-- = 4KB (4096B / 1024)
-- = 1/256MB (4KB / 1024)
on MB_GB(page) if page / 256 > 1024 then
(round page / 256 / 1024 * 10 rounding down) / 10 & "GB"
else
(round page / 256 rounding down) & "MB"
end if
end MB_GB

on join(sourceList, delimiter) set oldDelimiters to AppleScript's text item delimiters
set AppleScript's text item delimiters to delimiter
set theText to sourceList as text
set AppleScript's text item delimiters to oldDelimiters
return theText
end join

on vm_stat() "vm_stat|head -n12|tail -n11|sed 's/Pages/pages_/'|tr -d ' .\"-'|tr '\n\r' ','|sed 's/,$//'"
run script "{" & (do shell script result) & "}"
end vm_stat

on message(title, msg) try
do shell script "/usr/local/bin/growlnotify -m " & quoted form of msg & space & quoted form of title & " 2>&1"
if result is not "" then error -128
on error
activate
display alert title message msg giving up after 4
end try
end message


以上で、とりあえず完成。メモリは定期的に割とガッツリ解放されるようになった!しばらく4GBで耐えてみようと思う。

所感

  • コードの本質部分は、たったの3行。
  • でも、使い勝手を考えた修飾部分を含めると72行。
  • 元の24倍になってしまった...。
  • でも、これってよくあること。

効果



934MB - 473MB = 461MB解放された!


重複を排除する

  • コードの繰り返し部分を徹底して排除した。
  • diskutil repairPermissions /; purge;の順で実行した方が、メモリが効率良く解放されるような気がしたので修正した。
  • du -sx /; については、メモリを余分に消費してしまう場合もあったので、手順から取り除いた。

purgeコマンドがインストールされていない場合の対策

  • try_shell_script()ハンドラを追加して、エラーを明示するようにした。


ログ機能

  • 監視する10分ごとのタイミングで、vm_statの情報を記録しておくことにした。
    • 例:アプリケーション名がfree_memoryなら、~/Documents/free_memory.log に記録される。
  • 1000行を超えたら、ログローテーションするようにした。
    • free_memory.log > free_memory.log.1、free_memory.logには最新の100行だけ残る。



property MIN_FREE : 500 --監視するfree領域の最低容量(MB
property COMMAND_COMMENTS : {¬ {command:"diskutil repairpermissions /", comment:"3分ほどバックグラウンドで処理します。"}, ¬ {command:"purge", comment:"15秒ほど操作を受け付けない状態になります。"}} --{command:"du -sx / >& /dev/null & sleep 5 && kill $!", comment:"処理中です..."}, ¬
global COMMANDS

on run
set COMMANDS to {} repeat with cm in COMMAND_COMMENTS
set COMMANDS's end to cm's command
end repeat
--idle {} --不要
--my main() --デバッグ
end run

on idle
my vm_log() set vm to my vm_stat() if ( (vm's pages_free) + (vm's pages_speculative) ) / 256 < MIN_FREE then
my main() my vm_log() end if
return 600 --10分毎に実行する
end idle

on reopen
my main() my vm_log() end reopen

on main() activate
{"メモリ解放のため、以下のコマンドを実行します。", "(purge実行中の15秒間は、一時停止します。)", ""} display dialog my join(result & COMMANDS, return) giving up after 15
set msg to {} set msg's end to " at " & my datetime() set msg's end to my memory_info(my vm_stat()) repeat with cm in COMMAND_COMMENTS
my message(cm's command, cm's comment) my try_shell_script(cm's command) delay 5
set msg's end to my memory_info(my vm_stat()) end repeat
set msg's end to " at " & my datetime() activate
display alert "メモリ状態の遷移" message my join(msg, return) giving up after 60
end main

on vm_log() set log_text to my datetime() & " " & my memory_info(my vm_stat()) set log_path to (path to documents folder)'s POSIX path & my name & ".log"
do shell script "echo " & quoted form of log_text & " >> " & log_path
--1000行を超えたら、ログローテーションする。(xxxx.log > xxxx.log.1xxxx.logには最新の100ログだけ残る)
"f=" & quoted form of log_path & "; if [ `cat \"$f\"|wc -l` -ge 100 ]; then mv \"$f.1\" \"$f.2\"; mv \"$f\" \"$f.1\"; cat \"$f.1\" | tail -n 10 >\"$f\"; fi;"
do shell script result
end vm_log

on try_shell_script(command) try
do shell script command
on error msg number num
activate
" " & command & " が実行できません。\n\n" & msg & return & num
display dialog result buttons {"OK"} default button "OK" with icon caution giving up after 15
end try
end try_shell_script

on datetime() do shell script "date '+%Y-%m-%d %H:%M:%S'"
end datetime

on memory_info(r) "Free: " & my MB_GB( (r's pages_free) + (r's pages_speculative) ) & ¬ " Inactive: " & my MB_GB(r's pages_inactive) & ¬ " Active: " & my MB_GB(r's pages_active) & ¬ " Wired: " & my MB_GB(r's pages_wireddown) end memory_info

--1 page = 4096B
-- = 4KB (4096B / 1024)
-- = 1/256MB (4KB / 1024)
on MB_GB(page) if page / 256 > 1024 then
(round page / 256 / 1024 * 10 rounding down) / 10 & "GB"
else
(round page / 256 rounding down) & "MB"
end if
end MB_GB

on join(sourceList, delimiter) set oldDelimiters to AppleScript's text item delimiters
set AppleScript's text item delimiters to delimiter
set theText to sourceList as text
set AppleScript's text item delimiters to oldDelimiters
return theText
end join

on vm_stat() "vm_stat|head -n12|tail -n11|sed 's/Pages/pages_/'|tr -d ' .\"-'|tr '\n\r' ','|sed 's/,$//'"
run script "{" & (do shell script result) & "}"
end vm_stat

on message(title, msg) try
do shell script "/usr/local/bin/growlnotify -m " & quoted form of msg & space & quoted form of title & " 2>&1"
if result is not "" then error -128
on error
activate
" " & title & return & return & msg
display dialog result buttons {"OK"} default button "OK" with icon note giving up after 4 --with icon note caution stop
end try
end message

  • 上記コードは以下の形式で保存しておく必要がある。
    • ファイルフォーマット:アプリケーション
    • オプション:「実行後、自動的に終了しない」チェックあり