2009/06/18


WindowsではGrowl For Windowsとそれが使っているプロトコルGNTPにより、Windowsでもアイコンを使ったGrowlアプリケーションの開発が可能になりました。その一つにmiyagawaさんが作ったgithub growlerのGNTP版でもあるyet another github growlerというのも作りました。
これでMac, Windowsでのgithub growlerがある事になるのですが、Linuxにありません。Linuxにはアイコンが表示できてGNTPプロトコルを喋るGrowlシステムがありません。
そこで以前から使っていた、Growlネットワークプロトコル(Growlネットワークプロトコルはアイコンが出せません)をサポートしているmumblesというGrowlシステムを調べて見たところ、内部ではpythonでDBusによるプロセス間通信を行っている事が分かりました。
mumbles-project.org

a plugin driven, modern notification system for Gnome

http://www.mumbles-project.org/
さらにそのDBusインタフェース上ではアイコン表示をサポートしていた為、これは!と思いGitHubのGrowlアプリケーションを作ってみました。
まず、DBusで通信する為のプラグインを作成します。
DBusでメソッドが呼ばれると、MumblesPluginクラスに渡されるMumblesNotifyオブジェクトのalertメソッドを呼び出します。
全体のソースは以下の様になります。
from MumblesPlugin import *
import dbus
import gnomevfs
import os
import urllib

class GithubMumbles(MumblesPlugin):
  plugin_name = 'GithubMumbles'
  dbus_interface = 'com.github.DBus'
  dbus_path = '/com/github/DBus'
  icons = {'github' : 'github.png'}
  __url = None

  def __init__(self, mumbles_notify, session_bus):
    self.signal_config = {
      'Notify': self.Notify,
      'NotifyNum': self.NotifyNum
    }
    MumblesPlugin.__init__(self, mumbles_notify, session_bus)
    self.add_click_handler(self.onClick)

  def NotifyNum(self, num):
    self.__url = 'http://github.com/'
    icon = self.get_icon('github')
    title = 'Github'
    msg = str(num)+' new messages!'
    self.mumbles_notify.alert(self.plugin_name, title, msg, icon)

  def Notify(self, link, author, text):
    self.__url = link
    path = os.path.join(PLUGIN_DIR_USER, 'icons', 'github-%s' % author)
    if os.path.exists(path):
      self.icons[author] = 'github-%s' % author
      icon = self.get_icon(author)
    else:
      icon = self.get_icon('github')
    self.mumbles_notify.alert(self.plugin_name, author, text, icon)

  def onClick(self, widget, event, plugin_name):
    if event.button == 3:
      self.mumbles_notify.close(widget.window)
    else:
      self.open_url(self.__url)

  def open_url(self, url):
    mime_type = gnomevfs.get_mime_type(url)
    application = gnomevfs.mime_get_default_application(mime_type)
    os.system(application[2] + ' "' + url + '" &')
インタフェースはリンク、作者、本文のみとしました。これをegg形式にビルドしてmumblesのpluginフォルダに置くと、上記のインタフェース呼び出しによりGrowlが表示されます。
MumblesPluginにはget_iconメソッドが用意されており、これにはアイコン名称を渡す事になります。実際にはplugin/iconsというフォルダにある名称のファイルが使用されるので、今回の仕組としてはgithubフィードのチェッカースクリプトでアイコンをplugin/iconsフォルダに格納させ、それを使用してプラグイン側が使用するという形になっています。プラグイン側でアイコンを取って来ても良いのですがアイコンをダウンロードしている最中はGrowlが固まってしまう為、今回の様な作りとなっています。
次にチェッカースクリプトですが以下の様なコードになります。 #!/usr/bin/env python

UPDATE_INTERVAL=1000 # 10 minutes
MAX_NOTIFICATIONS = 40
DEBUG = True
##################################################

import os
import sys
import time
import getopt
import rfc822
import calendar
import urllib
import feedparser
import dbus
import dbus.service
from dbus.mainloop.glib import DBusGMainLoop
import gobject
from BeautifulSoup import BeautifulSoup
from pit import Pit

GITHUB_DBUS_INTERFACE = 'com.github.DBus'
GITHUB_DBUS_PATH = '/com/github/DBus'

config = Pit.get('github.com', {'require': {
    'user'  : 'user id on github.com',
    'token' : 'user token on github.com'
    }})

class Usage(Exception):
  def __init__(self, msg=None):
    app = sys.argv[0]
    if msg != 'help':
        self.msg = app+': Invalid options. Try --help for usage details.'
    else:
      self.msg = app+": DBus notifications on new github messages.\n"

class GithubCheck(dbus.service.Object):
  def __init__(self):
    session_bus = dbus.SessionBus()
    bus_name = dbus.service.BusName(GITHUB_DBUS_INTERFACE, bus=session_bus)
    dbus.service.Object.__init__(self, bus_name, GITHUB_DBUS_PATH)

    self.interval = UPDATE_INTERVAL
    self.notifyLimit = MAX_NOTIFICATIONS
    self.debug = DEBUG

    self.lastCheck = None
    self.minInterval = 60000 # 1 minute min refresh interval

    if self.interval < self.minInterval:
      print "Warning: Cannot check github more often than once a minute! Using default of 1 minute."
      self.interval = self.minInterval
    self._check()

  @dbus.service.signal(dbus_interface=GITHUB_DBUS_INTERFACE, signature='sss')
  def Notify(self, link, author, text):
    pass

  @dbus.service.signal(dbus_interface=GITHUB_DBUS_INTERFACE, signature='i')
  def NotifyNum(self, num):
    pass

  def _check(self):
    if self.debug:
      if self.lastCheck:
        print "checking feed (newer than %s):" %(self.lastCheck)
      else:
        print "checking feed:"
    try:
      items = feedparser.parse("http://github.com/%s.private.atom/?token=%s" % (config['user'], config['token']))['entries']
    except Exception, e:
      items = []

    if self.lastCheck:
      lastCheck = calendar.timegm(time.localtime(calendar.timegm(rfc822.parsedate(self.lastCheck))))
      for item in items:
        if calendar.timegm(item.published_parsed) < lastCheck:
          items.remove(item)

    self.lastCheck = rfc822.formatdate()
    num_notifications = len(items)

    if num_notifications > MAX_NOTIFICATIONS:
      if self.debug:
        print "%s new entries\n" %(num_notifications)
      self.NotifyNum(num_notifications)
    elif num_notifications < 0:
      if self.debug:
        print "no new entries\n"
    else:
      for item in items:
        path = os.path.join(os.path.expanduser('~'), '.mumbles', 'plugins', 'icons', 'github-%s' % item['author'])
        if not os.path.exists(path):
          html = urllib.urlopen('http://github.com/%s' % item['author']).read()
          soup = BeautifulSoup(html)
          img = soup.findAll('div', {'class':'identity'})[0].find('img')['src']
          img = img.replace("?s=50&", "?s=30&");
          urllib.urlretrieve(img, path)
        self.Notify(item['link'], item['author'], item['title'])
        time.sleep(6)
    gobject.timeout_add(self.interval,self._check)

if __name__ == '__main__':
  DBusGMainLoop(set_as_default=True)
  try:
    try:
      opts, args = getopt.getopt(
        sys.argv[1:], "hp", ["help"])
    except getopt.GetoptError:
      raise Usage()

    for o, a in opts:
      if o in ("-h", "--help"):
        raise Usage('help')
      else:
        raise Usage()
  except Usage, err:
    print >> sys.stderr, err.msg
    sys.exit(2)

  t = GithubCheck()
  try:
    loop = gobject.MainLoop()
    loop.run()
  except KeyboardInterrupt:
    print "githubcheck shut down..."
  except Exception, ex:
    print "Exception in githubcheck: %s" %(ex)
だらだらとしたコードですが、大体分かってもらえるかと思います。pitを使っているので初回起動のみユーザとトークンをエディタで入力する必要があります。トークンはGitHubのダッシュボードにあるRSSアイコンのリンク先URLに含まれています。

実行してしばらくすると以下の様な画面が表示されます。
mumbles-github-growler
これで快適になりました。
github上で全てのソースを公開しています。
mattn's mumbles-github-growler at master - GitHub

github growler using mumbles plugin and checker script.

http://github.com/mattn/mumbles-github-growler/tree/master
よろしければどうぞ。
Posted at by



2009/06/13


追記
パッチがいるのを忘れていました。
一番下にあります。

バッチファイル。実行すると直接バイナリ配置するので気をつけて。バッチの先頭にあるパスを環境に合わせ修正して下さい。
pythonのソースはオフィシャルから「Python-2.6.2.tar.bz2」を、pdcursesもオフィシャルから「pdcurs34.zip」をダウンロードし"C:¥TEMP¥"に展開しておきます。
@echo off
setlocal

set PYTHON26=c:\Python26

set PYSOURCE=c:\temp\Python-2.6.2
set PDSOURCE=c:\temp\pdcurses

if "%1" == "msvc" goto msvc
if "%1" == "gcc" goto gcc
goto usage

:msvc
set FLAGS=/nologo /LD -DHAVE_CURSES_RESIZE_TERM -DNCURSES_MOUSE_VERSION=2
echo building _curses.pyd
cl %FLAGS% -I%PYTHON26%\include -I%PDSOURCE% %PYSOURCE%\Modules\_cursesmodule.c /link %PYTHON26%\libs\python26.lib %PDSOURCE%\win32\pdcurses.lib %PDSOURCE%\win32\panel.lib /out:%PYTHON26%\lib\_curses.pyd
echo building _curses_panel.pyd
cl %FLAGS% -I%PYTHON26%\include -I%PDSOURCE% %PYSOURCE%\Modules\_curses_panel.c /link %PYTHON26%\libs\python26.lib %PDSOURCE%\win32\pdcurses.lib %PDSOURCE%\win32\panel.lib /out:%PYTHON26%\lib\curses\_curses_panel.pyd
goto end

:gcc
set FLAGS=-DHAVE_CURSES_RESIZE_TERM -DNCURSES_MOUSE_VERSION=2 -Wl,--enable-auto-import -Wl,--export-all -s -shared
echo building _curses.pyd
gcc %FLAGS% -I%PYTHON26%\include -I%PDSOURCE% %PYSOURCE%\Modules\_cursesmodule.c %PYTHON26%\libs\libpython26.a %PDSOURCE%\win32\pdcurses.a %PDSOURCE%\win32\panel.a -o %PYTHON26%\lib\curses\_curses.pyd
echo building _curses_panel.pyd
gcc %FLAGS% -I%PYTHON26%\include -I%PDSOURCE% %PYSOURCE%\Modules\_curses_panel.c %PYTHON26%\libs\libpython26.a %PDSOURCE%\win32\pdcurses.a %PDSOURCE%\win32\panel.a -o %PYTHON26%\lib\curses\_curses_panel.pyd
goto end

:usage
echo pycurses_build.bat [msvc or gcc]

:end
endlocal
以下ソースにあてるパッチです。Windows上のsetuptermは必ずエラーで返すコードになっているので呼ばないようにしています。
--- Modules/_cursesmodule.c.orig    2009-06-13 01:30:02.000000000 +0900
+++ Modules/_cursesmodule.c 2009-06-13 01:30:23.000000000 +0900
@@ -2036,6 +2036,7 @@
        }
    }
 
+#ifndef _WIN32
    if (setupterm(termstr,fd,&err) == ERR) {
        char* s = "setupterm: unknown error";
        
@@ -2048,6 +2049,7 @@
        PyErr_SetString(PyCursesError,s);
        return NULL;
    }
+#endif
 
    initialised_setupterm = TRUE;
 
Posted at by



2009/06/11


Tagtterをリリースして、初期は「CPU Quota」連発してしまい見苦しい状態でした。ごめんなさい。
さて、一時はどうなるかと思いましたが現在は「CPU Quota」も表示される事無く動いています。
今日はこの状況を乗り切る際に行った「Google App Engine」のパフォーマンスチューニングtipsを3つほどご紹介。

webapp.WSGIApplicationのdebugフラグはFalseにすべし

いきなり当たり前で申し訳ないですが、実はこのフラグがTrueかFalseかというだけで「CPU Quota」が出る頻度が極度に異なります。
実際に同じ症状の方もいらっしゃる様なので、おそらく間違いありません。
また公開する様なシステムでは、パスワード等も管理される事になるかと思いますが、debug=Trueだとスタックトレースに変数の値が表示されてしまい無茶苦茶危険です。気を付けましょう。
なお、Tagtterではユーザのパスワードは一切保存していません。

時間の掛かる既知な処理はキャッシュする

これはGoogle App Engineだけに言える話ではないですが、Tagtterではユーザの存在確認の為にtwitter.comへアクセスしています。
但し、タグが追加される度、voteされる度にこれを行ってしまうとtwitter.comにも負荷を掛ける事になります。
そこでTagtterではTagtter内に存在するユーザに対してはtwitter.comへのアクセスを行わないようになっています。
確かにGoogle App Engineの処理能力は良いのですが、いかんせん制限が厳しかったりします。余談になりますが現状Tagtterのデータ全件をXMLエクスポートする処理は「CPU Quota」で動きません。現在はデータの種別毎にバックアップしています。

Templateにはクラスオブジェクトは渡さない

Django Templateには、db.Modelのインスタンスを渡せて非常に便利なのですが、このDBインスタンスをTemplateの中から扱うと非常に遅くなります。
またdb.Modelに持ったgeneratorをTemplateから使うと更にパフォーマンスが落ちます。
例えばTagtterではユーザに対して付けられたタグをManyToManyで管理しており、モデルの一部を抜粋すると class TagtterTag(db.Model):
  name = db.StringProperty(required=True)

  ... snip ...

class TagtterMembership(db.Model):
  user = db.ReferenceProperty(TagtterUser)
  tag = db.ReferenceProperty(TagtterTag)

  ... ship ...

class TagtterUser(db.Model):
  name = db.StringProperty(required=True)

  ... snip ...

  def tags(self):
    return (x.tag for x in self.tagttermembership_set)

この様なコードになるのですが、user.tagsをTemplate側から呼び出すとかなりパフォーマンスダウンになります。また、Google App EngineではCPU占有度により「CPU Quota」させる仕組みがあるのですが、実はこれはTemplate内の呼び出しにも適応される為 application → template → model procedure という処理を行うと、1処理(1値参照)あたりのCPU使用回数が増える事になります。
つまり上記Tagtterのtagsページでは全てのタグが表示されますがこれを     template_values = {
      'tags' : TagtterTag.all(),
    }
この様にしてしまうとTemplate側からModelのメソッドを呼び出す事になり、現状のデータ件数だと100%エラーが発生してしまいます。
Google App EngineではCPU占有状態が5秒程続くとエラーになる様で、このブリッジ状態だけでもパフォーマンスダウンに繋がるって事になります。
これを回避する為には、applicationにてTemplateで使う値を全て保持してしまう事が最も効果的でした。
つまり全てdictとして渡してあげるのです。変にTemplate側で「タグの数が0でない物を表示」とするのではなく     all_tags = []
    for tag in TagtterTag.all():
      if tag.size():
        size = tag.size()
        all_tags.append({
          'name' : tag.name,
          'title' : tag.title,
          'size' : size,
          'fontsize' : font_size(size),
        })
    ... ship ...

    template_values = {
      ... ship ...

      'tags' : all_tags,

      ... ship ...
    }
この様にTemplate側からdb.Modelメソッドを呼び出させないようにするのです。 これで、現状エラーも出なくなりました。要するに極力db.Modelインスタンスにアクセスしない様にするってのが味噌ですね。
こんな品祖貧祖なパフォーマンスチューニングですが、皆さんの何かしらのお役に立てば幸いです。

他にもパフォーマンスチューニングの方法は幾らでもあるかと思います。Tagtterで言えばクライアント側の処理は殆どパフォーマンスチューニングされていないのが現状です。
不定期ですが時間を見付けてやって行きたいと思います。

何かご要望などあれば、Feature Requestまで。
Posted at by