PR

AppNapライクにWebProcessをコントロール(cputhrottle & applescript)

Firefoxをセーフモードで使用するようにしてから、
safariのWebProcessがCPU使用率の上位に位置することが多くなりました。

裏に隠れている場合にCPUの使用率を低下させてくれるという
次期OS X Mavericksには、App Napという魅力的な省電力機能があります。

processの使用上限を設定できる
cputhrottleというコマンドを見つけました。

これを使って、App Napライクなことができるのでは?と鈍く閃いたwので試してみます。

やりたいこと

AppNapライクを使えるならば、Firefoxをセーフモード→通常モードで使用する。

AppNap-browser applescriptの機能

CPUの制御対象はsafari(WebProcess), firefoxに限定する

  • safariがアクティブの場合は、WebProcessのCPU の上限を指定しない。
  • safariにフォーカスがない場合は、WebProcessのCPU上限を5%に制限する。
  • firefoxがアクティブの場合は、FirefoxのCPUの上限を指定しない。
  • firefoxにフォーカスがない場合は、FirefoxのCPU上限を5%に制限する。

準備

cputhrottleのインストール

cputhrottleをダウンロード後、コンパイル済みのバイナリがgz形式で保存されています。
> gzip -dc cputhrottle.gz
で解凍できます。
(私の環境では、拡張子がgzですが、実行形式だったようです。
 cp -p cputhrottle.gz cputhrottleでうまくいきました)

実行するために実行権限を与える必要があります。
> chmod +x cputhrottle

また、実行には、管理者権限が必要なため、sudoで実行する必要があります。
ひとまず、実行形式を/tmp/cputhrottleに置きました。

WebProcessを相手に挙動を確認したところcputhrottleで指定後、
CPU 制限中にcputhrottleプログラムは、常時生きている必要があるようです。
(Ctrl+Cで終了させて、制御終了)

AppleScript

ウィンドウの状態を監視し、cputhrottleを呼び出す
AppNap-browser.app(applescript)を作成します。

ウィンドウのアクティブ状態が変化した!というイベントを検出する方法が
わかりませんでした。
そのため、on idleで常に監視する苦肉の策で対応します。

AppNap-browser.app

global CPU_SLOW_PERCENTAGE
global myProcessDataList

set CPU_SLOW_PERCENTAGE to "5"
set myProcessDataList to {}

script ProcessData
	property pid : -1
	property procName : ""
	
	on new(thePid, theProcName)
		copy me to theData
		set_pid(thePid) of theData
		set_procName(theProcName) of theData
		theData
	end new
	on set_pid(value)
		set pid to value
	end set_pid
	on set_procName(value)
		set procName to value
	end set_procName
end script

on FindProcessDataList(theList, theProcName, retList)
	repeat with v in theList
		if procName of v is equal to theProcName then
			set end of retList to v
		end if
	end repeat
	return retList
end FindProcessDataList

on DeleteProcessDataList(theList, theProcName)
	set retList to {}
	repeat with v in theList
		if procName of v is not equal to theProcName then
			set end of retList to v
		end if
	end repeat
	return retList
end DeleteProcessDataList

on StartBackgroundCpuControl(theProcName, theControledList)
	syslog("StartBackgroundCpuControl  " & theProcName & "") of me
	tell application "System Events"
		set pList to (unix id of every process whose name contains theProcName)
		if pList is not equal to {} then
			repeat with unixID in pList
				syslog("cputhrottle  " & (unixID as string) & " " & CPU_SLOW_PERCENTAGE) of me
				do shell script "/tmp/cputhrottle " & (unixID as string) & " " & CPU_SLOW_PERCENTAGE & " >/dev/null 2>&1 & echo $!" with administrator privileges
				set pid to result
				set end of theControledList to new(pid, theProcName) of ProcessData
			end repeat
		end if
	end tell
end StartBackgroundCpuControl

on StopBackgroundCpuControl(theControledList)
	syslog("StopBackgroundCpuControl  targetCount=" & (length of theControledList as string) & "") of me
	tell application "System Events"
		repeat with v in theControledList
			do shell script "kill -INT " & (pid of v as string) with administrator privileges
		end repeat
	end tell
end StopBackgroundCpuControl

on idle
	tell application "System Events"
		set activeProcs to every process whose frontmost is true
		set activeProc to item 1 of activeProcs
		tell activeProc
			set activeProcName to name
		end tell
	end tell
	if activeProcName is equal to "Safari" then
		set targetProcess to {}
		set targetProcess to FindProcessDataList(myProcessDataList, "WebProcess", targetProcess)
		if length of targetProcess is not equal to 0 then
			StopBackgroundCpuControl(targetProcess)
			set myProcessDataList to DeleteProcessDataList(myProcessDataList, "WebProcess")
		end if
	else if activeProcName is equal to "Firefox" then
		set targetProcess to {}
		set targetProcess to FindProcessDataList(myProcessDataList, "Firefox", targetProcess)
		if length of targetProcess is not equal to 0 then
			StopBackgroundCpuControl(targetProcess)
			set myProcessDataList to DeleteProcessDataList(myProcessDataList, "Firefox")
		end if
	else
		set targetProcess to {}
		set targetProcess to FindProcessDataList(myProcessDataList, "WebProcess", targetProcess)
		if length of targetProcess is equal to 0 then
			-- 制御中は、再実行しない
			StartBackgroundCpuControl("WebProcess", myProcessDataList)
		end if
		set targetProcess to {}
		set targetProcess to FindProcessDataList(myProcessDataList, "Firefox", targetProcess)
		if length of targetProcess is equal to 0 then
			-- 制御中は、再実行しない
			StartBackgroundCpuControl("Firefox", myProcessDataList)
		end if
	end if
	return 1
end idle

on syslog(logmsg)
	do shell script "logger -s " & logmsg
end syslog

ファイルフォーマット:アプリケーション、オプションとして「ハンドラの実行時に終了しない」を選んで、書き出します。

こちらに新しいバージョンのスクリプトがあります。(追記)
AppNapライクにWebProcessをコントロール(cputhrottle & applescript)v2.0


AppNap-browser.appは、ウィンドウを持ちません。そのため終了するためには
ターミナルからkillコマンドを打ち込む必要があります。
> ps -el | grep AppNap-browser
でPIDを調べて、kill -9 調べたPID で終了させます。
CPU 制御中に終了させた場合、cputhrottleプロセスが生きている可能性があります。
同様に、kill -INTで終了させてください。

実行してみました

system.logに制御を書き出すようにしているので、
ログを見ながら挙動を確認します。

safariがアクティブウィンドウの場合、WebProcess 100%の指示を出しています。
safariではないアプリがアクティブウィンドウの場合、WebProcess 5%の指示を出していることが確認できます。

safari, firefox、その他ウィンドウにアクティブウィンドウを切り替えながらCPUの使用率を
確認(miniUsageを使用)してみると、
若干遅れて、5%制御されているのが確認できました。

この方法の課題は、

  1. Administrator privilegesを指定するため、
    キャッシュが消えたころにパスワードを促すダイアログが表示されて
    面倒に感じる

ですね。
Administrator privilegesのキャッシュ期間はどこで定められているのでしょうか・・

ローテクな手段ですが、AppNapもどき完成です^^

新しいバージョンのスクリプトがあります。
AppNapライクにWebProcessをコントロール(cputhrottle & applescript)v2.0
AppNapライクにSafari,Firefoxをコントロール(cputhrottle & applescript)v3.0

タイトルとURLをコピーしました