hogeとはワイルドカードのようなものです。日々起こった、さまざまなこと −すなわちワイルドカード− を取り上げて日記を書く、という意味で名付けたのかというとそうでもありません。適当に決めたらこんな理由が浮かんできました。
08/22/2012 ふむ [長年日記]
■ [Py][Tips] Python スクリプトのボトルネックを調査する
もう結構前の話だけれど,etherws を使っていて,「server -> client はそうでもないけど client -> server がやたら遅い」という現象を発見して,はて何でだろう? ということでボトルネックになっている箇所を調査した.
使うのは cProfile と pstats という 2 つのモジュール. これらをそれぞれプロファイリングと結果の表示に使う.
まず etherws を以下のようにして cProfile 機能 on で起動し,
# python -m cProfile -o etherws.prof etherws.py sw --foreground
VM 間を繋ぐ コレ と同じ環境を作り,しばらく全力で通信する.
etherws を Ctrl-C 等で停止すると,引数で指定した etherws.prof というファイルが転がっているので,pstats を使ってデータを表示する.
$ python -m pstats etherws.prof
インタラクティブシェルが起動するので,時間でソートして上位 10 件を表示してみる.
etherws.prof% sort time
etherws.prof% stats 10
Thu Aug 23 00:17:39 2012 etherws.prof
4496160 function calls (4495690 primitive calls) in 471.329 seconds
Ordered by: internal time
List reduced from 965 to 10 due to restriction <10>
ncalls tottime percall cumtime percall filename:lineno(function)
41524 191.581 0.005 191.581 0.005 {method 'poll' of 'select.epoll' objects}
41472 127.394 0.003 248.920 0.006 /usr/lib64/python2.7/site-packages/websocket.py:292(mask)
124419 109.251 0.001 109.251 0.001 {map}
124480 24.141 0.000 24.142 0.000 {method 'join' of 'str' objects}
41603 4.311 0.000 4.311 0.000 {range}
41472 2.042 0.000 267.134 0.006 /usr/lib64/python2.7/site-packages/websocket.py:288(_get_masked)
41475 1.195 0.000 1.195 0.000 {method 'send' of '_socket.socket' objects}
64106 0.612 0.000 272.782 0.004 etherws.py:220(send)
64106 0.582 0.000 276.070 0.004 etherws.py:244(_forward)
41472 0.566 0.000 268.295 0.006 /usr/lib64/python2.7/site-packages/websocket.py:258(format)
まぁ epoll とかはしょうがないとして,気になるのは以下で,呼び出し回数 (ncalls) の割に消費時間 (tottime) や下位の関数呼び出しも含めた消費時間 (cumtime) が多い.
41472 127.394 0.003 248.920 0.006 /usr/lib64/python2.7/site-packages/websocket.py:292(mask)
で,websocket.py の該当箇所を読んでみると,
@staticmethod
def mask(mask_key, data):
"""
mask or unmask data. Just do xor for each byte
mask_key: 4 byte string(byte).
data: data to mask/unmask.
"""
_m = map(ord, mask_key)
_d = map(ord, data)
for i in range(len(_d)):
_d[i] ^= _m[i % 4]
s = map(chr, _d)
return "".join(s)
WebSocket プロトコルのマスクを行う箇所のようで,data が個々のフレームを指しているようだ.
確かにこの処理内容だとフレームのコピーが数回発生することになる. Ether フレーム 1 個につきこれが毎度呼ばれると考えると,通信速度に影響してきそうだ,ということが分かる.
そこで何とかならんかなと色々試した挙げ句,以下の様な改造を施すことで,速度を大体 1.25 倍くらいには改善することができた. 1.25 倍というのは例えば 500Mbps を 625Mbps にするということで,たった数行の改造によって得られる効果としては充分だろう.
@@ -23,6 +23,7 @@
import socket
from urlparse import urlparse
import os
+import array
import struct
import uuid
import hashlib
@@ -309,12 +310,11 @@
data: data to mask/unmask.
"""
- _m = map(ord, mask_key)
- _d = map(ord, data)
- for i in range(len(_d)):
+ _m = array.array("B", mask_key)
+ _d = array.array("B", data)
+ for i in xrange(len(_d)):
_d[i] ^= _m[i % 4]
- s = map(chr, _d)
- return "".join(s)
+ return _d.tostring()
class WebSocket(object):
"""
...というのを pull request していたのだけれど,merge して頂けたようなので (ありがとうございます),その経緯を書いてみた次第.
ちなみに「server -> client はそうでもないけど client -> server がやたら遅い」という非対称性は WebSocket プロトコルの問題 (client -> server 通信時はデータをマスクすることが MUST だが逆はマスクしてはいけない) だったので解決不能だった :-P