未来のいつか/hyoshiokの日記

hyoshiokの日々思うことをあれやこれや

elispを眺めてみた

twitterにアクセスするのにtwittering-modeなるものを使っているのだがどうもタイムラインを更新する時に重たい感じがする。しかし、長年Emacsを利用はしているがelispにはとんと疎いのでどこがどう重いのかtwittering-mode.elを眺めても皆目見当がつかない。つーか、twittering-mode.elをどこに置いたのかすら忘れていてだめだめである。/usr/share/emacsのどこかに置いたかと思うのだけど全然見つからない。なんと、/etc/emacsにおいておいた。大丈夫か自分という感じである。(とほほ)

どこで時間がかかっているかを知るには実行プロファイルをとるのが王道中の王道である。elisp performance tuningなどでぐぐるが全然思うように情報が見つからない。多分、絶対elispのプロファイラがあるはずだと思うのだが探し出せない。

なんのことはない、elisp profilerでぐぐって、山本和彦さんのhttp://www.mew.org/~kazu/doc/elisp/profile.html を発見した。そこで早速elpを利用してみた。

M-x elp-instrument-package

ここでtwittering-といれる。パッケージ名の先頭を入れておくと頭がtwittering-な関数すべてがプロファイルの対象になるようである。

M-x elp-results

で結果が "*ELP Profiling Results*" バッファに合計時間で並べ替られて表示される。

Function Name                             Call Count  Elapsed Time  Average Time
========================================  ==========  ============  ============
twittering-http-get-default-sentinel      19          53.987238     2.8414335789
twittering-render-friends-timeline        19          30.305835000  1.5950439473
twittering-format-status                  1129        28.148776999  0.0249324862
twittering-get-response-body              19          15.483321     0.8149116315
twittering-timer-action                   19          10.219740999  0.5378811052
twittering-friends-timeline               19          10.217067000  0.5377403684
twittering-http-get                       19          9.240164      0.4863244210
twittering-xmltree-to-status              19          7.3414859999  0.3863939999
twittering-status-to-status-datum         380         7.2881300000  0.0191792894
twittering-cache-status-datum             380         0.4088780000  0.0010759947
twittering-decode-html-entities           2280        0.4062390000  0.0001781750
twittering-image-type                     1129        0.1648270000  0.0001459937
twittering-get-response-header            19          0.1051270000  0.0055330000
twittering-get-or-generate-buffer         132         0.0171760000  0.0001301212
twittering-buffer                         20          0.013266      0.0006633
twittering-http-buffer                    76          0.0075500000  9.934...e-05
twittering-wget-buffer                    36          0.0067700000  0.0001880555
twittering-mode                           1           0.00591       0.00591
twittering-mode-init-variables            1           0.004641      0.004641
twittering-start                          1           0.000443      0.000443

上位3つの関数で6割以上の実行時間を消費している。

そこで当該関数を写経する。

(defun twittering-http-get-default-sentinel (proc stat &optional suc-msg)
  (let ((header (twittering-get-response-header))
	  (body (twittering-get-response-body))
	  (status nil)
	  )
      (if (string-match "HTTP/1\.[01] \\([a-z0-9 ]+\\)\r?\n" header)
	  (progn
	    (setq status (match-string-no-properties 1 header))
	    (case-string
	     status
	     (("200 OK")
	      (mapcar
	       #'twittering-cache-status-datum
	       (reverse (twittering-xmltree-to-status
			 body)))
	      (twittering-render-friends-timeline)
	      (message (if suc-msg suc-msg "Success: Get.")))
	     (t (message status))))
	(message "Failure: Bad http response.")))
  )

defunは関数を定義するあれだ。(defun 関数の名前 (引数のリスト) 関数の定義)みたいな形式になる。この例ではtwittering-http-get-default-sentinelが関数の名前で(proc stat &optional suc-msg)が引数のリストで(let ... )が関数の定義だ。

次のletというのはlet (bindings...) forms...という形式をとる。

ここの(bindings...)は下記だ。

((header (twittering-get-response-header))
 (body (twittering-get-response-body))
 (status nil))

headerを(twittering-get-response-header)に、bodyを(twittering-get-response-body)そしてstatusをnilにする。まあ、ローカル変数に代入するようなものか。

で次がforms...に対応するところで、関数の本体みたいなものか。

(if (string-match "HTTP/1\.[01] \\([a-z0-9 ]+\\)\r?\n" header)
	  (progn
	    (setq status (match-string-no-properties 1 header))
	    (case-string
	     status
	     (("200 OK")
	      (mapcar
	       #'twittering-cache-status-datum
	       (reverse (twittering-xmltree-to-status
			 body)))
	      (twittering-render-friends-timeline)
	      (message (if suc-msg suc-msg "Success: Get.")))
	     (t (message status))))
	(message "Failure: Bad http response."))

ということで(if ...)がなにやらやっていることがわかる。まあ通常のプログラミング言語を知っていればif文というかifの構文が何をするかは大体わかるかと思う。

elispでのifはif 条件 then-form else-forms...という形式だ。

(string-match "HTTP/1\.[01] \\([a-z0-9 ]+\\)\r?\n" header)が条件だ。headerがHTTP....の形式になっているか調べている。もしこの条件が真ならば、下記を実行する。

	  (progn
	    (setq status (match-string-no-properties 1 header))
	    (case-string
	     status
	     (("200 OK")
	      (mapcar
	       #'twittering-cache-status-datum
	       (reverse (twittering-xmltree-to-status
			 body)))
	      (twittering-render-friends-timeline)
	      (message (if suc-msg suc-msg "Success: Get.")))
	     (t (message status))))

偽だったら(message "Failure: Bad http response.")を実行する。ここでmessageというのはその名のとおり、メッセージを表示する関数である。

さて、(progn...)の中身を見ていこう。prognは、そこにあるlisp関数を順番に実行するようなものである。ここの例では、(setq...)(case-string...)を実行する。

setq status (match-string-no-properties 1 header)

setqはstatusを(match-string-no-properties 1 header)を評価した値にする。さてmatch-string-no-propertiesは最後の探索や一致操作で一致したテキストを文字列として返す。1というのは1番目の括弧で囲んだ部分式に対応する部分のみを返す。 \\([a-z0-9 ]+\\)のところだ。

case-stringというのは何か。elispの関数ではない。

(defmacro case-string (str &rest clauses)
  `(cond
    ,@(mapcar
       (lambda (clause)
	 (let ((keylist (car clause))
	       (body (cdr clause)))
	   `(,(if (listp keylist)
		  `(or ,@(mapcar (lambda (key) `(string-equal ,str ,key)) keylist))
		't)
	     ,@body)))
       clauses)))

というマクロが定義されていた。

うう〜〜〜一気に難しくなったなあ。ということでマクロの定義のところままで、写経したtwittering-modeの読書はひとまず休憩とする。