負荷テストその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)); } [...] }
テストの内容としては、
- GroovyObject.getProperty(String)を毎回実行
- GroovyObject.getMetaClass().getProperties()を最初に実行して、結果を最初に取っておいて、MetaProperty.getProperty(Object)のみを毎回実行
- 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)
まだまだだけども、前の結果と見比べて欲しい。