MacBook Air のターミナル(bash)が喋る

最近知ったのですが、ターミナルからsayコマンド打つと、なんとMacが喋ります。

Finder → アプリケーション → ターミナル.app を起動

コマンドラインに以下のように打ち込みます。

say "hello" 

Macが「ハロー」と発音します。
すごい!

なお、-vオプションで声を指定できます。

say -v V "hello" ← 女性
say -v D "hello" ← ホラー風
say -v G "hello" ← 歌声
say -v Z "hello" ← ロボット

この機能、使いようによっては仕事の役にも立ちます。

例)

残業したくないことを上司に伝えるとき

say -v F "mou ka e ri tai"

同僚が体調不良により戦線離脱したとき

say -v F "zawa ... zawa ..."

終電をすぎても帰宅できないとき

say -v F "kon na no ze tai oka shi yo"

残業代を貰うとき

say -v F "by push"

Capistrano(カピストラーノ)でPHPプロジェクトをデプロイ

Capistrano(カピストラーノ)は複数のリモートサーバにSSH接続して一括操作できる便利なツールです。
色々な用途に使えますが、今回はCapistranoPHPプロジェクトをデプロイしてみました。

概要図

Capistranoをインストールするサーバは一台だけです。
デプロイ先のアプリケーションサーバではログイン用アカウント(要sudo権限)を事前に作成しておきます。

インストール

上の図ではCapistranoソースコード管理サーバにインストールしています。
Capistranoを動かすにはRubyが必要になりますので、まずはRubyをインストールします。
Rubyのインストール方法は省略します。)
次に、以下のコマンドでCapistranoをインストール。

$ sudo gem install capistrano

使用方法

基本的にcapfileに一連の操作を記述し、あとはコマンド一発で実行です。
今回はdeploy.rbというファイルを用意しました。
deploy.rb

# 基本定義 
set :application,   "sample_app"    # アプリケーション名
set :user,          "deployer"      # sshでログインするユーザー(要sodo権限)
set :password,      "XXXXXX"        # パスワード

set :svn_repo,      "file:///usr/local/svn/repos/sample_app/trunk"  #svnリポジトリ
set :svn_pass,      "XXXXXX"                    # svnパスワード
set :checkout_dir,  "/home/deployer/checkout"   # checkout先ディレクトリ (*注意:ここで指定したディレクトリは一旦'rm -rf'で全消去されます)
set :tar_dir,       "/home/deployer"            # 圧縮ファイル置場
set :upload_dir,    "/home/deployer"            # 圧縮ファイルupload先ディレクトリ
set :deploy_dir,    "/var/www/sample_app"       # deploy先ディレクトリ (*注意:ここで指定したディレクトリは一旦'rm -rf'で全消去されます)

set :own_user,      "root"      # deploy後のown設定
set :own_group,     "root"      # deploy後のown設定


# サーバ定義 
role :cap, "localhost"      # ファイル管理サーバ
role :ap1, "192.168.10.2"  # アプリケーションサーバ(deploy先)


# check outタスク
task :checkout_, :roles => [:cap] do
    sudo "rm -rf #{checkout_dir}"
    sudo "mkdir #{checkout_dir}"
    sudo "svn co --username #{user} --password #{svn_pass} #{svn_repo} #{checkout_dir}"
    sudo "chown -R #{user} #{checkout_dir}"
    sudo "find #{checkout_dir} -name \".svn\" -type d | xargs rm -rf"
end

# 圧縮タスク
task :tar_, :roles => [:cap] do
    sudo "tar cvfz #{tar_dir}/dep.tar.gz -C #{checkout_dir} ."
    sudo "chown #{user} #{tar_dir}/dep.tar.gz"
end

# uploadタスク
task :upload_, :roles => [:ap1] do
    upload("#{tar_dir}/dep.tar.gz", "#{upload_dir}/dep.tar.gz", :via => :scp)
    sudo "rm -rf #{deploy_dir}"
    sudo "mkdir #{deploy_dir}"
    sudo "tar xvfz #{upload_dir}/dep.tar.gz -C #{deploy_dir}"
    sudo "chown -R #{own_user}:#{own_group} #{deploy_dir}"
    sudo "rm #{upload_dir}/dep.tar.gz"
end


task :deploy_, :roles => [:cap] do
    checkout_
    tar_
    upload_
end

以下のように実行します。

$ sudo cap -f deploy.rb deploy_


capfile(deploy.rb)解説

・基本定義
  ユーザーやパスワード、SVN設定、ディレクトリパスなどを定義しています。
・サーバ定義
  デプロイ元とデプロイ先のサーバを定義しています。
・check outタスク
 概要図の「check out」→「.svnディレクトリを削除」を実行します。
・圧縮タスク
 概要図の「圧縮」を実行します。
・uploadタスク
 概要図の「転送」→「前回のデプロイファイルを削除」→「展開」を実行します。

おわりに

上の例では使っていませんが、トランザクションロールバック機能などもあるようです。
Capistrano便利ですね!

参考サイト

http://doruby.kbmj.com/trinityt_on_rails/20080325/__Capistrano___1
http://d.hatena.ne.jp/japanrock_pg/20080729/

Google App Engine ログビューア

Google App Engine ログビューアを作成しました。
GAE Log Viewerには以下のような機能があります。

  • タイムゾーン切替(現地時間、PST、UTC
  • 日にち範囲指定
  • IP範囲指定
  • ログレベル指定
  • フィルタリング後のトータル件数表示
  • 2種類の表示モード(ログ表示、リクエスト表示)

対応ブラウザ(File APIが使えるブラウザ)

使い方

  • まずは、Google App Engineのログをダウンロードします。(参考
  • ダウンロードしたログファイルをGAE Log Viewerの"select log file"(画面左上)で選択します。
  • デフォルトでは現地時間(Locale)が表示されています。PSTでGoogleの管理画面と同じ時間になります。

今後、需要があるようでしたら以下の機能も追加しようと思います。

  • 時間ソート機能
  • 複数ファイルの読み込み機能(重複判定有り)
  • 前回のフィルタ条件保存機能

今回、ツールを作るにあたって参考にさせて頂いたサイト

タスクキュー実験

下の表に沿って実験したところ、リクエスト数:1 タスク数:512 wait:1000ms で「Error: Server Error」が発生しました。


Error: Server Error
The server encountered an error and could not complete your request.
If the problem persists, please report your problem and mention this error message and the query that caused it.

このとき、実行されなかったタスクが221あり、各タスクのエラーログは出力されていないのですが、これは親サーブレット(キューに投げる側)のほうで、タスクを追加するまえにエラー停止した為と思われます。
(テストで使用したソースコードこちら

リクエスト数タスク数wait(ms)成功失敗instance数処理時間(平均)処理時間(最大)
11100010
12100020
14100040
18100080
1161000160
1321000320
1641000640
112810001280
125610002560
15121000291221


【2010/3/2に追記】

すみません、上記のテストを実施した時から随分間が空き、SDKバージョンも1.3.1に変わってしまいました。。。

以下は、2010年3月2日時点でのテスト結果です。(rate:20/s bucket:5)

リクエスト数タスク数wait(ms)トライ失敗instance数全タスク完了までの時間(ms)
1110001011445
1210002012136
1410004014582
1810008025052
116100016046190
132100032049293
1641000640417382
112810001280545931
125610002560585935
1512100051207192966
1102410001024010284789
110241000102405385936


最後の行でインスタンス数が落ちているのは、spin downした為と思われます。

3月2日の時点ではインスタンス数は最大10ぐらいで高止まりするみたいです。



【2010/3/13に追記】

インスタンス数の話(情報元

・実際には長期的なトラフィックも見てて、一気にリクエストを発生させるようなスパイクでは本来の動きをしない。
・そういうテストをしたいとリクエストを出せば対応してくれる。
・30どころじゃないインスタンスで動作してるのもある
上記はGoogler直伝とのこと。インスタンス数の上限は長期的なトラフィックによっても変わるようですね。

タスクキュー調査用ソース

【注】1/27 18:30頃にソースを更新しました。
サーブレットインスタンスIDを取得するように変更)
メモ
・タスク数と処理時間を数パターン用意してテストする。
jmeterで複数同時にリクエストしてテストする。


以下のようにGETパラメータでタスク数と処理時間を指定(appidの部分は適宜変更)
ttp://appid.appspot.com/tqtest/Test00Servlet?num=4&wait=1000

Test00Servlet.java

package tqtest;
import java.util.UUID;
import java.util.logging.Logger;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.*;
import com.google.appengine.api.labs.taskqueue.Queue;
import com.google.appengine.api.labs.taskqueue.QueueFactory;
import com.google.appengine.api.labs.taskqueue.TaskOptions;

@SuppressWarnings("serial")
public class Test00Servlet extends HttpServlet {
	private static final Logger logger = Logger.getLogger(Test00Servlet.class.getName());
	private static String servletId = UUID.randomUUID().toString();

	public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		long start = System.currentTimeMillis();
		String uuid = UUID.randomUUID().toString();
		try {
			resp.setContentType("text/plain");
			int num = Integer.parseInt(req.getParameter("num"));
			String wait = req.getParameter("wait");
			String url = "/tqtest/Queue00Servlet?wait=" + wait  + "&rid=" + uuid;
			logger.info(String.format("%s,%s,Test00Servlet Start:%d", servletId, uuid, start));

			Queue q = QueueFactory.getDefaultQueue();
			for (int i=0; i<num; i++) {
				q.add(TaskOptions.Builder.url(url).method(TaskOptions.Method.GET));
			}
			PrintWriter out = resp.getWriter();
			out.println(String.format("queued num:%d wait:%sms", num, wait));
			out.close();
		} catch (Exception e) {
			logger.info(String.format("%s,%s,Test00Servlet Exception :%s", 
				servletId, uuid, e.toString()));
			throw new ServletException(e);
		} finally {
			logger.info(String.format("%s,%s,Test00Servlet End:%dms", 
				servletId, uuid, (System.currentTimeMillis() - start)));
		}
	}
}

Queue00Servlet.java

package tqtest;
import java.util.UUID;
import java.util.logging.Logger;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.*;

@SuppressWarnings("serial")
public class Queue00Servlet extends HttpServlet {
	private static final Logger logger = Logger.getLogger(Queue00Servlet.class.getName());
	private static String servletId = UUID.randomUUID().toString();

	public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		long start = System.currentTimeMillis();
		String wait = req.getParameter("wait");
		String rid = req.getParameter("rid");
		String uuid = UUID.randomUUID().toString();
		try {
			logger.info(String.format("%s,%s,%s,Start :%d", servletId, rid, uuid, start));
			Thread.sleep(Integer.parseInt(wait));
		} catch (Exception e) {
			logger.info(String.format("%s,%s,%s,Exception :%dms :%s", 
				servletId, rid, uuid, (System.currentTimeMillis() - start), e.toString()));
			throw new ServletException(e);
		} finally {
			logger.info(String.format("%s,%s,%s,End :%dms", 
				servletId, rid, uuid, (System.currentTimeMillis() - start)));
		}
	}
}

web.xml

<?xml version="1.0" encoding="utf-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
	<servlet>
		<servlet-name>Test00Servlet</servlet-name>
		<servlet-class>tqtest.Test00Servlet</servlet-class>
	</servlet>
	<servlet-mapping>
		<servlet-name>Test00Servlet</servlet-name>
		<url-pattern>/tqtest/Test00Servlet</url-pattern>
	</servlet-mapping>
	<servlet>
		<servlet-name>Queue00Servlet</servlet-name>
		<servlet-class>tqtest.Queue00Servlet</servlet-class>
	</servlet>
	<servlet-mapping>
		<servlet-name>Queue00Servlet</servlet-name>
		<url-pattern>/tqtest/Queue00Servlet</url-pattern>
	</servlet-mapping>

	<welcome-file-list>
		<welcome-file>index.html</welcome-file>
	</welcome-file-list>
</web-app>

makeAsyncCallによる非同期クエリの実験

[Google App Engine] makeAsyncCallによる非同期クエリの実験結果がこちらのブログで紹介されています。
大変興味深かったので、自分でも環境を作り、実際に測定してみました。

テストデータと仕様
 (shin1ogawaさんのブログから引用)

1-1000までのIDを持つキーを作成し、そのキーのIDの数値が2で割り切れるならmod2という属性の値がtrue、割り切れないならfalseというカンジでmod3,mod5とか用意した。で、クエリは「2または3または5で割り切れる」というものを抽出するという簡単なもの。条件的にはmod2 EQUAL true OR mod3 EQUAL true OR mod5 EQUAL trueとなり、分けるとmod2 EQUAL true、mod3 EQUAL true、mod5 EQUAL trueの3本を走らせて結果をマージするというもの。うん、でも本当はそんな細かいとこはどうでも良くて、重要なのはクエリを分割実行してマージする、しかもクエリは非同期で一気にみっつ実行するという点です。

http://shin1o.blogspot.com/2010/01/or-appengine.html

今回測定したのは以下の3種です。

 (A) Datastore.query版 (同期)
 (B) makeSyncCall版 (同期)
 (C) makeAsyncCall版 (非同期)

結果
タイプA BC
368377189
384387222
434386499
445385231
465391178
389548195
388383218
393393194
391391188
372401203
平均402.9404.2231.7

(↑それぞれ12回ずつGAE環境で実行し、最少と最大の値は除外しています。)

テスト用コードはshin1ogawaさんの黒魔術コードを、ほぼそのままお借りしました。
なお、新しく作ったmakeSyncCall版は以下の通りです。

void sync2or3or5a(PrintWriter w) throws InterruptedException, ExecutionException {
	Query q2 = new Query(KIND).addFilter("mod2", FilterOperator.EQUAL, true);
	Query q3 = new Query(KIND).addFilter("mod3", FilterOperator.EQUAL, true);
	Query q5 = new Query(KIND).addFilter("mod5", FilterOperator.EQUAL, true);
	FetchOptions fetchOptions = FetchOptions.Builder.withOffset(0).limit(1000).prefetchSize(1000);

	long start = System.currentTimeMillis();

	DatastorePb.Query qPB2 = PbUtil.toQueryRequestPb(q2, fetchOptions);
	DatastorePb.Query qPB3 = PbUtil.toQueryRequestPb(q3, fetchOptions);
	DatastorePb.Query qPB5 = PbUtil.toQueryRequestPb(q5, fetchOptions);

	Delegate<Environment> delegate = ApiProxy.getDelegate();
	Environment env = ApiProxy.getCurrentEnvironment();
	List<byte[]> results = new ArrayList<byte[]>();

	results.add(delegate.makeSyncCall(env, "datastore_v3", "RunQuery", qPB2.toByteArray()));
	results.add(delegate.makeSyncCall(env, "datastore_v3", "RunQuery", qPB3.toByteArray()));
	results.add(delegate.makeSyncCall(env, "datastore_v3", "RunQuery", qPB5.toByteArray()));

	List<List<Entity>> lists = new ArrayList<List<Entity>>();
	for (int i = 0; i < results.size(); i++) {
		byte[] bytes = results.get(i);
		DatastorePb.QueryResult rPb = new DatastorePb.QueryResult();
		rPb.mergeFrom(bytes);
		Iterator<EntityProto> it = rPb.resultIterator();
		List<Entity> entities = new ArrayList<Entity>();
		while (it.hasNext()) {
			entities.add(EntityTranslator.createFromPb(it.next()));
		}
		lists.add(entities);
	}
	w.println("count=" + merge(lists).size() + ", " + (System.currentTimeMillis() - start) + "[ms]");
}

*[JavaScript] Firefox3.5 プロトタイプチェーンの不具合(3.6 betaでは修正されています)

以下は、Firefox3.5でJavaScriptを実行した際に起こるプロトタイプチェーン処理の不具合の例です。
なお、この不具合は3.6 betaで修正されています。(Bugzillaで報告・確認済み)

<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script type="text/javascript">

var Foo = function(){};
Foo.prototype.hello = function(){ alert("0") };

var Bar = function(){};
Bar.prototype = new Foo();

var a = new Bar();
var b = new Bar();

a.hello(); // ← 0が表示される
b.hello(); // ← 0が表示される

Bar.prototype.hello = function(){ alert("1") };

a.hello(); // ← 1が表示される
b.hello(); // ← 正常なら1が表示されるはずだが、0が表示されてしまう。

</script>
</head></html>