WAITFOR(RECEIVE)

ここによるとMS SQLServerでService Brokerからのメッセージを受信する際にWAITFOR(RECEIVE)を使用すると、分散トランザクションを使用できないみたいだ(WAITFORは内部でSAVEPOINTを使用しているらしい)。
こんな例外が発生する。

System.Transactions.TransactionAbortedException: トランザクションが中止されました。
 ---> System.Transactions.TransactionPromotionException: トランザクションを進めるときにエラーが発生しました。
 ---> System.Data.SqlClient.SqlException: このトランザクションを分散トランザクションに昇格できません。アクティブな savepoint がこのトランザクション内に存在します。
...

JavaでeTokenを使う(その2)

まずは、eTokenでキーペアを作ってみる。providerとkeyStoreはすでに設定済みと言う前提で。

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.util.Calendar;
import javax.crypto.Cipher;
import sun.security.x509.AlgorithmId;
import sun.security.x509.CertificateAlgorithmId;
import sun.security.x509.CertificateIssuerName;
import sun.security.x509.CertificateSerialNumber;
import sun.security.x509.CertificateSubjectName;
import sun.security.x509.CertificateValidity;
import sun.security.x509.CertificateX509Key;
import sun.security.x509.X500Name;
import sun.security.x509.X509CertImpl;
import sun.security.x509.X509CertInfo;


...

public void doGenerateKeyPair(String alias, String principal, String storePass,
				int keyLength, BigInteger serial)
		throws Exception {
		
    KeyPairGenerator kpgen = KeyPairGenerator.getInstance("RSA", provider);
    kpgen.initialize(keyLength);
    KeyPair kp = kpgen.generateKeyPair();

    Calendar cal = Calendar.getInstance();
    Calendar expire = (Calendar)cal.clone();
    expire.add(Calendar.DATE, 3650);
		
    X509CertInfo info = new X509CertInfo();
		
    CertificateValidity validity =
            new CertificateValidity(cal.getTime(), expire.getTime());
    info.set(X509CertInfo.VALIDITY, validity);
    info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(serial));

    X500Name name = new X500Name(principal);
    CertificateIssuerName issuer = new CertificateIssuerName(name);
    CertificateSubjectName subject = new CertificateSubjectName(name);
    AlgorithmId alogoId = AlgorithmId.get(
            AlgorithmId.sha1WithRSAEncryption_oid.toString());

    info.set(X509CertInfo.ISSUER, issuer);
    info.set(X509CertInfo.SUBJECT, subject);
    info.set(X509CertInfo.KEY, new CertificateX509Key(kp.getPublic()));
    info.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(alogoId));

    X509CertImpl target = new X509CertImpl(info);
    target.sign(kp.getPrivate(), alogoId.getName(), provider.getName());

    keyStore.setKeyEntry(alias, (Key)kp.getPrivate(), storePass.toCharArray(),
                        new Certificate[] {target});
}

まず、以下の行でキーペアを作成する。eTokenで使用可能なのはキーはRSAなのでそれを指定する。キー長は1024ビット(eToken PRO 32k/64k)もしくは2048ビット(eToken PRO 64k)を指定する。ただし、2048ビットを指定するには、eToken PRO 64kの場合でも2048ビットRSAキーを使用できるようにeTokenが初期化されている必要がある。

KeyPairGenerator kpgen = KeyPairGenerator.getInstance("RSA", provider);
kpgen.initialize(keyLength);
KeyPair kp = kpgen.generateKeyPair();

次に、生成したキーペアの公開鍵に自己証明書を作成し、秘密鍵で署名する。ここで指定しているprincipalには、「CN=Common Name,OU=Your OU,U=Your U,C=JP」のような文字列を指定する。自己証明のため、ISSUERとSUBJECTには同じ名前が入る。

Calendar cal = Calendar.getInstance();
Calendar expire = (Calendar)cal.clone();
expire.add(Calendar.DATE, 3650);
		
X509CertInfo info = new X509CertInfo();
		
CertificateValidity validity =
        new CertificateValidity(cal.getTime(), expire.getTime());
info.set(X509CertInfo.VALIDITY, validity);
info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(serial));

X500Name name = new X500Name(principal);
CertificateIssuerName issuer = new CertificateIssuerName(name);
CertificateSubjectName subject = new CertificateSubjectName(name);
AlgorithmId alogoId = AlgorithmId.get(
        AlgorithmId.sha1WithRSAEncryption_oid.toString());

info.set(X509CertInfo.ISSUER, issuer);
info.set(X509CertInfo.SUBJECT, subject);
info.set(X509CertInfo.KEY, new CertificateX509Key(kp.getPublic()));
info.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(alogoId));

X509CertImpl target = new X509CertImpl(info);
target.sign(kp.getPrivate(), alogoId.getName(), provider.getName());

最後にこれらをkeyStore(eToken)にセーブする。

keyStore.setKeyEntry(alias, (Key)kp.getPrivate(), storePass.toCharArray(),
                    new Certificate[] {target});

実行すると、eTokenのランプがチカチカするのでよくわかる。

JavaでeTokenを使う(その1)

eTokenを使用するJavaのプログラムを作ってみる。eTokenを使用するには、eTokenを使用できるPKCS11のプロバイダーを経由して行う。たぶん。

import java.security.Provider;

public class AladdinTool {
    private static Provider provider;
    static {
        InputStream is = getConfigStream();
        try {
            provider = new sun.security.pkcs11.SunPKCS11(is);
            Security.addProvider(provider);
        }
        finally {
            try {
                is.close();
            }
            catch (Exception ex) {
            }
        }
    }
    private static InputStream getConfigStream() throws Exception {
        Map<String, String> env = System.getenv();
        String sysRoot = env.get("SystemRoot");
        String pkcsDll = null;
        if (sysRoot != null) {
            // Windows
            pkcsDll = sysRoot + "\\system32\\eTpkcs11.dll";
        }
        else {
            throw    new UnsupportedOperationException(
                            "This type of OS or JRE is not supported yet.");
        }
        String content =
                "library=" + pkcsDll + "\n" +
                "name=eToken\n" +
                "description=eToken PKCS#11 Dynamic Link Library\n" +
                "attributes=compatibility\n"
                ;
        return	new ByteArrayInputStream(content.getBytes());
    }
    public static KeyStore login(String pin) throws Exception {
        char[] pinarry = pin.toCharArray();
        KeyStore ks = KeyStore.getInstance("PKCS11", provider);
        ks.load(null, pin);
        return ks;
    }
}

面倒くさいので、getConfigStream()でeTokenのプロパティの内容を内部で作成するようにしてみた(Windowsでしか使えないけど)。内容はkeytoolでeTokenを使用するときに指定するプロパティファイルの中身と同じものとした。
後は、login()でパスワードを指定してKeyStoreを取得した後にエントリを取得すればいい。

eTokenを初期化する

ebayで買ったeToken PRO 64kが2048ビットRSAキーが使用できない状態で届いた(しかも色が赤になってる)。どうも一回初期化しないといけないようなので、初期化しようとしてみる。
eToken Utilities 2のフォーマットアプリで手元にあるeToken PRO 64kをなぜか初期化することができない。eTokenを挿してもフォーマットアプリが認識してくれないのだ。eToken PRO 32kは認識するのに。
で、eToken PKI Client 4をインストールして初期化をすると、eToken RTE 3.5では使用できなくなってしまった(認識するがログインできない)。どうも新しいドライバで初期化すると古いバージョンのドライバがインストールされている環境では使えないみたいなのだ。orz
そこでいろいろ調べてみた。
■eToken PKI Client 4でのフォーマット
レジストリのキー HKEY_LOCAL_MACHINE\SOFTWARE\Aladdin\eToken\MIDDLEWARE\Init に LEGACY-FORMAT-VERSION というDWORD値を作成し、0を設定するとeToken RTE 3.5環境でも使用可能なフォーマットで初期化される。でも、eToken PKI Clientで初期化すると色がunknownになっちゃうんですけどね。
■eToken RTE 3.5でフォーマット
レジストリのキー HKEY_LOCAL_MACHINE\SOFTWARE\Aladdin\eToken\eTProperties の Advanced のDWORD値を1から1fに変更すると、eToken properties に Initialization というメニューが出現する。ここからフォーマットができる模様。手元のeToken PRO 64kもフォーマットできるっぽい(まだ試してないけど)。

2007/11/15 追記 : きちんとフォーマット・使用できた。

NetBeans 6.0 Beta 2を使ってみる

NetBeans 6.0 beta 2が出ているのでダウンロードして使ってみることにした。
ダウンロードは、ここからZipフォーマットをダウンロードして使うことにした。というか、いつもZIPのフォーマットしかダウンロードしないんだけど。
展開後、etc/netbeans.confのnetbeans_default_userdirを修正してBeta 1の時と同じディレクトリ(一応バックアップを取っておく)を指定して起動したところ、何かおかしい。
よく見ると、Javaのソースエディターの文字が全部真っ黒に(よく見なくても気づけよ)。
ちなみに私はJavaのソースのフォントとか、色を自分でカスタマイズしている(昔のJBuilder風に)。おそらくそのせいなんだろうと思われる。
ユーザディレクトリをデフォルトの状態に戻して、5.5のユーザディレクトリから移行するように起動してみた。
やっぱりだめ。
すみません、Beta 2、速攻消しましたorz。
あとは、・・・いい加減このバグ直してくれ。4.0から2年間放置って。

Maven 2プラグインでアクションを変更する(NetBeans 6.0 Beta 1)

noryksj2007-10-23

NetBeans 6.0からMaven 2のプラグインが標準?でサポートされているようだ。
メニューのTools→Pluginsで出てくる一覧にMavenがあるし、Tools→OptionsでMiscellaneousを選ぶとMaven2というタブがちゃんと出てくる。
これのプラグインがインストールされていると、Maven 2のプロジェクトディレクトリをそのままNetBeansのプロジェクトとして扱えるようになるという優れものだが、Eclipseのものに比べてしまうと便利というわけでは決してない。
まず、普通にプロジェクトをビルドすると、ビルドした後にテストまでしてしまう。これはかなり鬱陶しい。次に、JUnitのテストを実行するといつものJUnit結果画面にならない。ま、これは慣れるかもしれないけど。
で、ビルド時にテストまでしないようにするには、プロジェクトのプロパティで、Actionsカテゴリを選択して、Build projectのゴールをinstallからcompileに、Clean and Build projectのゴールをclean compileにすると、テストを実行しないようになる。これでかなりストレスがたまらなくなる。
処理は全部Mavenに丸投げなのでしょうがないといえばそれまでなんだけど、なんとかならないもんでしょうかね。

負荷テストその3(Grails 0.5)

先日までの内容の負荷テストを元にちょっといじってみた。なお、以下の内容についての責任は一切負わないので自己の責任で。
まずは、コントローラの生成が重い点についてだが、どうもクロージャの生成が特に重いようだ。以下のメソッドを変えてみる。Groovyの1.1 BETA 1のソースをSVNで取ってきて変更する。
groovy.lang.MetaClassImplの以下のメソッド

   public int selectConstructorAndTransformArguments(int numberOfCosntructors, Object[] arguments) {
       //TODO: that is just a quick prototype, not the real thing!
       if (numberOfCosntructors != constructors.size()) {

       [...]

       List l = new ArrayList(constructors);
       Comparator comp = new Comparator() {
           public int compare(Object arg0, Object arg1) {
               Constructor c0 = (Constructor) arg0;
               Constructor c1 = (Constructor) arg1;
               String descriptor0 = BytecodeHelper.getMethodDescriptor(Void.TYPE, c0.getParameterTypes()); 
               String descriptor1 = BytecodeHelper.getMethodDescriptor(Void.TYPE, c1.getParameterTypes());
               return descriptor0.compareTo(descriptor1);
           }            
       };
       Collections.sort(l,comp);

       [...]
       return ret;
   }

「just a quick prototype」なので、重いのもしょうがないのかも。constructorsの内容自体は変更されることはないようだ。で、これを以下のように変更。

       List l = sortedConstructors;
       if (l == null) {
           l = new ArrayList(constructors);
           Comparator comp = new Comparator() {
               public int compare(Object arg0, Object arg1) {
                   Constructor c0 = (Constructor) arg0;
                   Constructor c1 = (Constructor) arg1;
                   String descriptor0 = BytecodeHelper.getMethodDescriptor(Void.TYPE, c0.getParameterTypes()); 
                   String descriptor1 = BytecodeHelper.getMethodDescriptor(Void.TYPE, c1.getParameterTypes());
                   return descriptor0.compareTo(descriptor1);
               }
           };
           Collections.sort(l,comp);
           sortedConstructors = l;
       }

インスタンス変数sortedConstructorsを追加して、そこに結果を保持するようにした。要はキャッシュした、ということ。

次に、コントローラのgetProperty()が重いことについての変更。
まずは、テストプログラムで検証。

package jp.ne.hatena.d.noryksj.test.groovy;

import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyObject;

public class PropertyLabo {
	String code =
		"class LaboLabo {\n" +
		"private String address\n" +
		"def message = 'Hello'\n" +
		"def square = {x -> x * x }\n" +
		"def getNow = {-> new java.util.Date() }\n" +
		"static staticSquare = {x -> x * x }\n" +
		"String getName() { return 'YOUR NAME' }\n" +
		"void setAddress(String address) {  }\n" +
		"}";
	GroovyClassLoader loader;
	
	public GroovyObject loadObject() throws Exception {
		if (loader == null) {
			loader = new GroovyClassLoader();
		}
		return	(GroovyObject)loader.parseClass(code).newInstance();
	}
}

以下のようなテストケースでテスト。

package jp.ne.hatena.d.noryksj.test.groovy;

[...]

public class PropertyLaboTest extends TestCase {
	static int COUNT = 300000;

	public PropertyLaboTest(String name) {
		super(name);
	}
	
	public void testSimpleProp() throws Exception {
		PropertyLabo labo = new PropertyLabo();
		GroovyObject obj = labo.loadObject();
		assertNotNull(obj);
		assertEquals("Hello", obj.getProperty("message"));
		long start, end;
		
		start = System.currentTimeMillis();
		for (int i = COUNT; i > 0; --i) {
			assertEquals("Hello", obj.getProperty("message"));
		}
		end = System.currentTimeMillis();
		System.out.println("01 Elapsed = " + (end - start));
	}
	
	public void testSimplePropCachedMP() throws Exception {
		PropertyLabo labo = new PropertyLabo();
		GroovyObject obj = labo.loadObject();
		
		MetaClass mc = obj.getMetaClass();
		List props = mc.getProperties();
		Map map = new HashMap(props.size());
		for (Iterator it = props.iterator(); it.hasNext(); ) {
			MetaProperty mp = (MetaProperty)it.next();
			map.put(mp.getName(), mp);
		}
        
		assertNotNull(obj);
		assertEquals("Hello", obj.getProperty("message"));
		long start, end;
		
		start = System.currentTimeMillis();
		for (int i = COUNT; i > 0; --i) {
			MetaProperty mp = (MetaProperty)map.get("message");
			assertEquals("Hello", mp.getProperty(obj));
		}
		end = System.currentTimeMillis();
		System.out.println("02 Elapsed = " + (end - start));
	}
	
	public void testSimplePropDirectMP() throws Exception {
		PropertyLabo labo = new PropertyLabo();
		GroovyObject obj = labo.loadObject();
		
		assertNotNull(obj);
		assertEquals("Hello", obj.getProperty("message"));
		long start, end;
		
		start = System.currentTimeMillis();
		for (int i = COUNT; i > 0; --i) {
			assertEquals("Hello", obj.getMetaClass().getProperty(obj, "message"));
		}
		end = System.currentTimeMillis();
		System.out.println("03 Elapsed = " + (end - start));
	}

	[...]
}

テストの内容としては、

  1. GroovyObject.getProperty(String)を毎回実行
  2. GroovyObject.getMetaClass().getProperties()を最初に実行して、結果を最初に取っておいて、MetaProperty.getProperty(Object)のみを毎回実行
  3. GroovyObject.getmetaClass().getProperty(Object, String)を毎回実行

で、結果は、

01 Elapsed = 843
02 Elapsed = 78
03 Elapsed = 204

となった。MetaProperty.getProperty()を実行するだけが一番速い。
なぜこんなに時間の差があるのかというと、多分、GroovyObject.getProperty(String)とMetaProperty.getProperty(Object)は等価ではないのだろう。ただし、今回のようなコントローラのプロパティ取得という限定された状況で、結果が同じならばより速い方法を採用してもいいかもしれない。取得するコントローラのプロパティはプログラム上ほぼ固定なので、その範囲内で等価な動作であるかを確認すればよいのではなかろうか?
で、とりあえず、GrailsUtil内にこんなユーティリティメソッドを作ってみた。

    public static Object getProperty(GroovyObject object, String name) {
//    	if (isDevelopmentEnv()) {
//    		return	object.getProperty(name);
//    	}
    	return	GPUTIL.getProperty(object, name, false);
    }

    private static GroovyPropertyUtil GPUTIL = new GroovyPropertyUtil();
    private static class GroovyPropertyUtil {
    	private Map cache = Collections.synchronizedMap(new HashMap());
    	private static final Object[] EMPTY = {};
    	
    	public Object getProperty(GroovyObject object, String name) {
    		MetaClass mc = null;
    		Class clazz = object.getClass();
    		Map mpMap = (Map)cache.get(clazz);
    		if (mpMap == null) {
    			mc = object.getMetaClass();
    			mpMap = buildMetaPropertyMap(mc);
    	        	cache.put(clazz, mpMap);
    		}
    		MetaProperty mp = (MetaProperty)mpMap.get(name);
    		if (mp == null) {
    			else if (mc == null) {
    				mc = object.getMetaClass();
    			}
    			return	mc.getProperty(object, name);
    		}
   		return	mp.getProperty(object);
    	}
    	
    	private void appendMetaBeanProperty(Map toAdd, List props) {
    		for (Iterator it = props.iterator(); it.hasNext(); ) {
    			MetaProperty mp = (MetaProperty)it.next();
    			toAdd.put(mp.getName(), mp);
    		}
    	}
    	
    	private Map buildMetaPropertyMap(MetaClass mc) {
    		Map mpMap = new HashMap();
    		appendMetaBeanProperty(mpMap, mc.getProperties());
    		return	mpMap;
    	}
    }

コントローラのプロパティを取得しているところをこのメソッドの呼出に置き換える。例えば、org.codehaus.groovy.grails.web.servlet.mvc.SimpleGrailsControllerHelperではコントローラのプロパティを取得しまくっている(ここでは割愛)。
変更してビルドするとエラーが。「Cannot read write-only property: constraints」だそうだ。
投げ元はMetaBeanPropertyのここ。

    public Object getProperty(Object object) {
        if (getter == null) {
            //TODO: we probably need a WriteOnlyException class
            throw new GroovyRuntimeException("Cannot read write-only property: " + name);
        }
        return getter.invoke(object, MetaClassHelper.EMPTY_ARRAY);
    }

ふむふむ、getterがないわけね。Write-Onlyのプロパティを取得するわけがないと思うのだけれど、先ほどのユーティリティをこんな風にしてみた。

    	private void appendMetaBeanProperty(Map toAdd, List props) {
    		for (Iterator it = props.iterator(); it.hasNext(); ) {
    			MetaProperty mp = (MetaProperty)it.next();
    			String name = mp.getName();
    			if (!(mp instanceof MetaBeanProperty)) {
    				continue;
    			}
    			MetaBeanProperty mbp = (MetaBeanProperty)mp;
    			final MetaMethod getter = mbp.getGetter();
    			if (getter == null) {
    				mp = new MetaProperty(name, mp.getType()) {
    					public Object getProperty(Object object) {
    						return	null;
    					}
    					public void setProperty(Object object, Object value) {
    						// Do nothing.
    					}
    				};
    			}
    			toAdd.put(mp.getName(), mp);
    		}
    	}

getterがなかったらnull返すMetaPropertyをキャッシュするようにしてみた。で、テスト。結果は同じ。getterあるのにないってどういうことだ!!
よくよく調べると、GroovyObject.getMetaClass()で返ってきているのはExpandoMetaClassで、このクラスは、getGetter()をオーバライドしていて常にgetterを返す。しかし、実際にgetProperty()を実行すると、親クラス側にはgetterがセットされていないらしい、ということみたいだ。
それから、それから、他にもまだまだたくさんあるが、とりあえず以下のようなコードにしている。

    	private void appendMetaBeanProperty(Map toAdd, List props) {
    		for (Iterator it = props.iterator(); it.hasNext(); ) {
    			MetaProperty mp = (MetaProperty)it.next();
    			String name = mp.getName();
    			if (!(mp instanceof MetaBeanProperty)) {
    				continue;
    			}
    			MetaBeanProperty mbp = (MetaBeanProperty)mp;
    			final MetaMethod getter = mbp.getGetter();
    			if (getter == null) {
    				mp = new MetaProperty(name, mp.getType()) {
    					public Object getProperty(Object object) {
    						return	null;
    					}
    					public void setProperty(Object object, Object value) {
    						// Do nothing.
    					}
    				};
    			}
    			else {
    				mp = new MetaProperty(name, mp.getType()) {
    					public Object getProperty(Object object) {
    						return	getter.invoke(object, EMPTY);
    					}
    					public void setProperty(Object object, Object value) {
    						// Do nothing.
    					}
    				};
    			}
    			toAdd.put(mp.getName(), mp);
    		}
    	}

getterがあったらそれを実行する、という内容にしてみた。
次に、DefaultGrailsControllerClassにある以下のような箇所。

	public boolean isInterceptedBefore(GroovyObject controller, String action) {
		final Map controllerProperties = DefaultGroovyMethods.getProperties(controller);
		return isIntercepted(controllerProperties.get(BEFORE_INTERCEPTOR),action);
	}

どうもDefaultGroovyMethods.getProperties(controller)で、コントローラのプロパティを一括してとって、その後にインタセプターがあるかを判定しているようだ。要は全部のプロパティを取得しているわけで、インタセプタの登録ない場合にNoSuchPropertyみたいな例外が上がらないようにということなのだろうか?これは、先ほどのMetaPropertyにインタセプタのMetaPropertyがないかを判定するだけでいいような気がする。
次に、ControllersGrailsPlugin.groovyのactionUriと、controllerNameを以下のように展開した。

			metaClass.getActionUri = {->
				('/' << RCH.currentRequestAttributes().controllerName << '/' << RCH.currentRequestAttributes().actionName).toString()
			}
			metaClass.getControllerUri = {->
				('/' << RCH.currentRequestAttributes().controllerName).toString()
			}

最後に、Grailsとは関係ないが、自前のHttpSessionの実装でフラッシュスコープの中身が存在しない場合には、フラッシュスコープを永続化しないように修正した。

これらの修正を施した結果は、

$ ab -c 300 -n 30000 http://localhost:8080/pagetest-0.1/test01
This is ApacheBench, Version 2.0.40-dev <$Revision: 1.146 $> apache-2.0
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Copyright 2006 The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 3000 requests
Completed 6000 requests
Completed 9000 requests
Completed 12000 requests
Completed 15000 requests
Completed 18000 requests
Completed 21000 requests
Completed 24000 requests
Completed 27000 requests
Finished 30000 requests


Server Software:        Apache-Coyote/1.1
Server Hostname:        localhost
Server Port:            8080

Document Path:          /pagetest-0.1/test01
Document Length:        90 bytes

Concurrency Level:      300
Time taken for tests:   38.879998 seconds
Complete requests:      30000
Failed requests:        0
Write errors:           0
Total transferred:      8097996 bytes
HTML transferred:       2700000 bytes
Requests per second:    771.60 [#/sec] (mean)
Time per request:       388.800 [ms] (mean)
Time per request:       1.296 [ms] (mean, across all concurrent requests)
Transfer rate:          203.40 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0  151 791.9      1   21018
Processing:     4  143  87.6    125    1342
Waiting:        0  138  86.9    119    1340
Total:         15  295 796.8    133   21132

Percentage of the requests served within a certain time (ms)
  50%    133
  66%    162
  75%    183
  80%    200
  90%    267
  95%    574
  98%   3154
  99%   3209
 100%  21132 (longest request)

まだまだだけども、前の結果と見比べて欲しい。