技術

Macで重いバックグラウンドアプリケーションを劇的に軽くするPythonスクリプト

誇張ではありません。なんせSIGSTOP使っちゃってますからw 劇薬w

僕はメインのMacでは常時ブラウザを3つ立ち上げてます。
管理用(ログインしてなんやかんやするやつ)としてFirefoxを、主に情報収集用としてもう一つFirefoxを、プログラミング用としてChromeを使ってます。
このうち2つのFirefoxでそれぞれ常に50〜100タブぐらいひらきっぱなしにしてるのですが、これがバックグラウンドでもCPUにかなりの負荷を掛けてます。
重すぎるので情報収集用なんかはFlashとJavaScriptをオフ(FlashblockとNoScriptを使用)にしてるくらいですが、それでもアイドル時のCPU使用率の上位二つはFirefoxです。

これをなんとかしようと思いまして、随分前にTwitterのほうにシグナルでアプリケーションを一時停止・再開する方法を載せましたが、

killall -SIGSTOP firefox-bin
killall -SIGCONT firefox-bin

いちいちコマンド打つのもめんどくさい。アプリケーション化してクリックで済ますと言ってもネェ。

そこでアプリケーションが最前面にあるかどうかを随時調べつつ、いい感じでSIGSTOP・SIGCONTしたら楽というか意識する必要すらないよねと思いまして、AppleScriptで実現できないか模索したこともあったのですが、AppleScriptのあまりの変態言語っぷりに挫折。

それからFirefoxのタブを整理したりしてCPU使用率がガマンできるレベルまで下がったので今まで忘れてたんですが、PythonからAppleScriptと同じような事が出来る(AppleScriptを操作できる?)というappscriptというパッケージがあることを今日知りまして、とりあえず実装してみた次第であります。
appscriptはeasy_installとかで簡単に入ります。

こんな感じ。appsl.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

'''
application sleeper
'''

import os, sys, time
from subprocess import Popen, PIPE
from appscript import *

interval = 0.5

def main():
	process_name = sys.argv[1]
	my_filename = os.path.basename(__file__)
	
	ps = Popen(['ps', 'x'], stdout = PIPE)
	grep = Popen(['grep', process_name], stdin = ps.stdout, stdout = PIPE)
	result = grep.communicate()[0]
	procs = []
	for line in result.split('\n'):
		if 'grep' not in line and my_filename not in line and process_name in line:
			pid = int(line.split()[0])
			procs.append(dict(pid = pid, app = app(pid = pid), cont = True))
	
	try:
		while True:
			time.sleep(interval)
			for proc in procs:
				os.system('/bin/kill -s SIGCONT %d' % proc['pid'])
				if proc['app'].frontmost.get():
					# frontmostなら停止しない
					if not proc['cont']:
						proc['cont'] = True
						print 'continue %s' % proc['pid']
				else:
					# frontmostじゃないなら停止
					os.system('/bin/kill -s SIGSTOP %d' % proc['pid'])
					if proc['cont']:
						proc['cont'] = False
						print 'stop %s' % proc['pid']
	except KeyboardInterrupt, e:
		pass
	finally:
		# 終わる時はすべて再開しておく
		print 'destructing...'
		for proc in procs:
			os.system('/bin/kill -s SIGCONT %d' % proc['pid'])
			print 'continue %s' % proc['pid']
		
if __name__ == '__main__':
	main()

Firefox専用じゃないんでとりあえず普通のウインドウを持つアプリケーションならなんでも使えるかと。

使い方はこんな感じ。

python appsl.py firefox-bin

当然ながらバックグラウンドでの処理が重要な意味を持つアプリケーションでは使うべきではないです。
あと長期間のテストは行ってないです。
以前killallコマンドだけで試してみたときはSTOPしたままMac自体を長時間スリープし復帰すると、SystemUIServerのCPU利用率が上がりデスクトップ全体が重くなるという現象を確認しています。
その際CONTさせると正常な状態に戻ることも確認しています。
今回のスクリプトでは対象がバックグラウンドの場合でも0.5秒ごとにCONT→STOPをやってますので、同じような問題が起きてもあまり影響はないかななんて思ってます。

表示中のタブ以外は処理を止める、という機能がFirefoxにあると一番いいんですが、以前Firefoxのえらい人が「バックグラウンドのタブのGmailで新着メールの確認が出来なくなる」的な事を理由に実装しないという噂を聞いたことがあるので、望みは薄いです。
ただChromeと同じように、タブごとにプロセスを分けるようなプロジェクトもぼちぼち進んでるみたいなので、そっちが使えるようになったら「表示中のタブ以外は処理を止める」というアドオンも出てくるかもしれません。いつになるかは分かりませんが。
というか最初からタブごとにプロセスが分かれてるChromeにならもうあるかも。
でも、Firefoxの「ツリー型タブ」アドオンに匹敵するくらいの縦タブが使えないとメインで使う気にはならないです。
(Windows版でなら縦タブが使える事をちょっと前に確認してますが、見た目も使い勝手もまだまだうんこです。)

コメント

BarTabすばらしいですね!

このエントリを書く以前、大量のタブを管理するタイプのアドオン(タブグループマネージャとか)を幾つか試していたのですが、いまいちUIが好きになれなくて、そんなんだったら標準のブックマーク機能でいいんじゃね?とか思ってました

ですがBarTabは標準のUIに与える影響も最小限で、十分な効果を発揮してくれますね

教えていただきありがとうございます

こんなエラーが出るんですが、原因は何でしょう?
osは10.6.8です。
File “./appsl.py”, line 32, in main
if proc[‘app’].frontmost.get():
File “/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/aeosa/appscript/reference.py”, line 579, in __getattr__
raise AttributeError(“Unknown property, element or command: %r” % name)

エラーメッセージを見る限りAppleScript側のapplicationオブジェクトにfrontmostプロパティ見つからないので、Python側のappscriptパッケージでエラーが発生しているように見えます。
ただその場合エラーメッセージの最後は
raise AttributeError(“Unknown property, element or command: %r” % name)
ではなく
AttributeError: Unknown property, element or command: ‘frontmost’
となるはずなので、何か別の問題が起きてるような気もしますが正直私にはよく分からないです。

ありがとうございます。
SheepShaverでは動かないようです。SheepShaverの起動前にスクリプトを走らせるとエラーは出ないのですが、stop/continueの表示が出ず動いてません。
SheepShaverの起動中に実行するとターミナルが前面なのに以下のメッセージが出て前述のエラーが表示されます。
destructing…
continue 9826

SfariやSeaMonkeyではばっちりです。すばらしい。
SheepShaverが裏の時に止めたかったのに残念です。
frontmostのテストではアプリ名が出るので前面にあることは取得出来ているようですが。
frontmostのテストでSheepShaverをフルパスで指定しないとUnable to find application named ‘SheepShaver’になるのが気になります。
killall SheepShaverは動くのに。

なるほど…SheepShaverというアプリ(Power Macintoshエミュ)で動作しないということですね。

もしかしたら
SheepShaverがSIGCONTシグナルに対応してない(もしくはSIGSTOPしてない状態でSIGCONTシグナルを受ける事に対応してない)ため、
スクリプト中で kill -s SIGCONT したタイミングでSheepShaverが停止(もしくは一時停止?)してしまい(このとき destructing… のメッセージが表示され)
その影響でAppleScript側のSheepShaverのapplicationオブジェクトが無効になり、frontmostプロパティにアクセスできずPython側で例外が発生する、
ということが起きてるのではないかと思いました。

もしそうだとすると問題の回避は難しそうですね…
私にはちょっと回避法が思いつきません。

Unable to find application named ‘SheepShaver’の件についても問題に関係してそうな気がしますが
これの原因についても思い当たるものがないです。

SheepShaverを使った事がなく試せる環境も今手元にない(2~3年前にMac使いを辞めました)ため、確認することもできず以上は私の想像でしかないのですが…

applescriptだとアプリに依存しないで動きますね。control-cやcomman+.で強制終了するときにSIGCONTしておく方法がわかりませんが。

適当なディレクトリでosascript ./appsl.scpt Safari

on run argv
	if (count of argv) > 0 then
		set theArg to argv as text
		repeat
			delay 1.0
			tell application "System Events"
				set a to name of (path to frontmost application)
			end tell
			set savedDelimiters to AppleScript's text item delimiters
			set AppleScript's text item delimiters to "."
			set front_app to items 1 thru -2 of text items of a as text
			set AppleScript's text item delimiters to savedDelimiters
			if front_app is equal to theArg then
				log "continue"
				do shell script "killall -SIGCONT " & theArg
			else
				log "stop"
				do shell script "killall -SIGSTOP " & theArg
			end if
		end repeat
	else
		display dialog "not specified app"
		return
	end if
	do shell script "killall -SIGCONT " & theArg
end run

スクリプトの投稿ありがとうございます。

私のスクリプトとは処理内容が少し違うようですがAppleScriptだとそういうふうに書けるんですね~
勉強になります。

そのスクリプトで問題なくお使いになれるということであれば以下は余計な話かもしれませんが、
今Macを引っ張り出して少し調べて少し思った事がありますので一応書いておきます(私が残しておきたいので)。

いただいたスクリプトでは
System Eventsのname of (path to frontmost application)で取得したアプリ名(例えばSafari.app)から .app を取り除いてkillallに渡してますが、
本来killallに渡すべきはコマンド名(アプリケーションバンドル内部の実行ファイル名)なので、アプリによっては動かない場合があるかもです。
(実際は実行ファイル名をアプリ名に合わせてある事が多い気がしますが…ただFirefoxだと多分違ったはずです。)

一応この辺は私のスクリプトではappscript pythonパッケージで自動的に吸収されるようにはなっていますが
そのappscriptについて今ちょっとgithubのソースをチラッと見たところ、
内部的にはCのCarbonAPI?のGetProcessForPID関数とGetProcessBundleLocation関数を呼び出して
プロセスIDをアプリケーションバンドルのパスに変換してるようで
この辺りの関数がアプリによって動かない場合があるとすると、
先日教えていただいたようなエラーが出るのかもしれません。
(完全に原因は突き止めてないですがSheepShaverを対象にした際に教えていただいたようなエラーが再現することは確認しました。)

私のスクリプトでは指定のアプリが最前面にあるかどうかを確認するためだけにappscript pythonパッケージを使ってますので、
その部分を別の手段に置き換えればSheepShaverでも動作する可能性があります。

例えば、引数に渡したアプリがfrontmostかどうかだけを調べtrueかfalseかだけ出力し即時終了するAppleScriptを書いて、
それをPython側から実行し結果を読み取る、とかです。
これならcontrol-cなどで終了した際の終了処理もそのまま動作するはずです。

AppleScriptでも終了処理をする方法があれば、全てAppleScriptで完結することもできますね。

コメントを残す

メールアドレスが公開されることはありません。



※画像をクリックして別の画像を表示