diff --git a/src-impl/org/seasar/mayaa/impl/CONST_IMPL.java b/src-impl/org/seasar/mayaa/impl/CONST_IMPL.java index 68c2abe1..8622f131 100644 --- a/src-impl/org/seasar/mayaa/impl/CONST_IMPL.java +++ b/src-impl/org/seasar/mayaa/impl/CONST_IMPL.java @@ -37,6 +37,7 @@ public interface CONST_IMPL { String DEBUG = "org.seasar.mayaa.debug"; String CHECK_TIMESTAMP = "checkTimestamp"; + String CHECK_TIMESTAMP_INTERVAL = "checkTimestampInterval"; String DEFAULT_SPECIFICATION = "defaultSpecification"; String SUFFIX_SEPARATOR = "suffixSeparator"; String WELCOME_FILE_NAME = "welcomeFileName"; diff --git a/src-impl/org/seasar/mayaa/impl/builder/injection/XPathMatchesInjectionResolver.java b/src-impl/org/seasar/mayaa/impl/builder/injection/XPathMatchesInjectionResolver.java index 40e2c820..2e8c8bac 100644 --- a/src-impl/org/seasar/mayaa/impl/builder/injection/XPathMatchesInjectionResolver.java +++ b/src-impl/org/seasar/mayaa/impl/builder/injection/XPathMatchesInjectionResolver.java @@ -34,6 +34,7 @@ import org.seasar.mayaa.impl.CONST_IMPL; import org.seasar.mayaa.impl.NonSerializableParameterAwareImpl; import org.seasar.mayaa.impl.engine.EngineUtil; +import org.seasar.mayaa.impl.engine.specification.SpecificationImpl; import org.seasar.mayaa.impl.engine.specification.SpecificationUtil; import org.seasar.mayaa.impl.engine.specification.xpath.XPathUtil; import org.seasar.mayaa.impl.util.StringUtil; @@ -54,6 +55,13 @@ public class XPathMatchesInjectionResolver extends NonSerializableParameterAware private static final CopyToFilter _xpathFilter = new CheckXPathCopyToFilter(); + // TODO テンプレートのprefix定義、Mayaaファイルのを反映させる + private static final Namespace XPATH_NAMESPACE; + static { + XPATH_NAMESPACE = SpecificationUtil.createNamespace(); + XPATH_NAMESPACE.addPrefixMapping("m", CONST_IMPL.URI_MAYAA); + } + protected CopyToFilter getCopyToFilter() { return _xpathFilter; } @@ -89,34 +97,54 @@ public SpecificationNode getNode( throw new IllegalArgumentException(); } - // TODO テンプレートのprefix定義、Mayaaファイルのを反映させる - Namespace namespace = SpecificationUtil.createNamespace(); - namespace.addPrefixMapping("m", CONST_IMPL.URI_MAYAA); - - // mayaaファイル内のm:xpathを持つすべてのノードを対象とする Specification spec = SpecificationUtil.findSpecification(original); - List injectNodes = new ArrayList<>(); - while (spec != null) { - SpecificationNode mayaa = SpecificationUtil.getMayaaNode(spec); - if (mayaa != null) { - getXPathNodes(mayaa, injectNodes); - } - spec = EngineUtil.getParentSpecification(spec); + if (spec == null) { + // original がどの spec にも属さない場合は injection 不可 + return chain.getNode(original); } - for (Iterator it = injectNodes.iterator(); it.hasNext();) { - SpecificationNode injected = it.next(); - String mayaaPath = SpecificationUtil.getAttributeValue( - injected, QM_XPATH); - // injectedをnamespaceとして渡すとfunctionのデフォルトnamesapceが - // URI_MAYAAになってしまうため、デフォルトnamespaceのないものを渡す - if (XPathUtil.matches(original, mayaaPath, namespace)) { - return injected.copyTo(getCopyToFilter()); + // spec 階層を上向きに辿り、各 spec の xpath inject ノードを順に試す + Specification cur = spec; + while (cur != null) { + List injectNodes = null; + if (cur instanceof SpecificationImpl) { + injectNodes = ((SpecificationImpl) cur).getCachedXpathInjectNodes(); + if (injectNodes == null) { + injectNodes = buildXpathInjectNodes(cur); + ((SpecificationImpl) cur).setCachedXpathInjectNodes(injectNodes); + } + } else { + injectNodes = buildXpathInjectNodes(cur); + } + for (Iterator it = injectNodes.iterator(); it.hasNext();) { + SpecificationNode injected = it.next(); + String mayaaPath = SpecificationUtil.getAttributeValue(injected, QM_XPATH); + // injectedをnamespaceとして渡すとfunctionのデフォルトnamesapceが + // URI_MAYAAになってしまうため、デフォルトnamespaceのないものを渡す + if (XPathUtil.matches(original, mayaaPath, XPATH_NAMESPACE)) { + return injected.copyTo(getCopyToFilter()); + } } + cur = EngineUtil.getParentSpecification(cur); } return chain.getNode(original); } + /** + * 1つの spec の .mayaa ノードだけを走査し、m:xpath ノードのリストを構築する。 + * + * @param spec 対象の spec + * @return m:xpath を持つ injection ノードのリスト(この spec のみ) + */ + protected List buildXpathInjectNodes(Specification spec) { + List nodes = new ArrayList<>(); + SpecificationNode mayaa = SpecificationUtil.getMayaaNode(spec); + if (mayaa != null) { + getXPathNodes(mayaa, nodes); + } + return nodes; + } + /** * .mayaaファイルパース後に呼び出す。m:mayaa直下でないノードにxpath属性がある場合、 * 警告を一度だけ記録する。{@link SpecificationBuilderImpl#afterBuild} から呼ばれる。 diff --git a/src-impl/org/seasar/mayaa/impl/engine/specification/SpecificationImpl.java b/src-impl/org/seasar/mayaa/impl/engine/specification/SpecificationImpl.java index bec72693..b1e06ce2 100644 --- a/src-impl/org/seasar/mayaa/impl/engine/specification/SpecificationImpl.java +++ b/src-impl/org/seasar/mayaa/impl/engine/specification/SpecificationImpl.java @@ -15,10 +15,14 @@ */ package org.seasar.mayaa.impl.engine.specification; +import java.util.Collections; import java.util.Date; import java.util.Iterator; +import java.util.List; import java.util.Objects; +import org.seasar.mayaa.engine.specification.SpecificationNode; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.seasar.mayaa.builder.SpecificationBuilder; @@ -46,8 +50,17 @@ public class SpecificationImpl extends ParameterAwareImpl implements Specificati private transient SourceDescriptor _source; private volatile NodeTreeWalkerImpl _delegateNodeTreeWalker; private boolean _hasSource; - private boolean _deprecated = true; + private volatile boolean _deprecated = true; private int _lastSequenceID; + private volatile long _lastExistsCheckMillis = 0; + private volatile long _existsCheckIntervalMillis = -1L; + + + /** + * XPath injection ノードキャッシュ(この spec の .mayaa に含まれる m:xpath ノード一覧)。 + * {@code deprecate()} 呼び出し時にクリアされ、次のビルド時に再構築される。 + */ + private volatile List _xpathInjectNodes; protected boolean needCheckTimestamp() { return EngineUtil.getEngineSettingBoolean(CONST_IMPL.CHECK_TIMESTAMP, true); @@ -57,6 +70,22 @@ protected boolean isSourceExists() { return getSource().exists(); } + /** + * ソース存在チェック間隔(ミリ秒)を返します。 + * 設定値は初回呼び出し時にのみ取得し、インスタンスフィールドにキャッシュします。 + */ + protected long getExistsCheckIntervalMillis() { + if (_existsCheckIntervalMillis < 0) { + String val = EngineUtil.getEngineSetting(CONST_IMPL.CHECK_TIMESTAMP_INTERVAL, "500"); + try { + _existsCheckIntervalMillis = Long.parseLong(val); + } catch (NumberFormatException e) { + _existsCheckIntervalMillis = 500L; + } + } + return _existsCheckIntervalMillis; + } + protected SpecificationBuilder getBuilder() { return ProviderUtil.getSpecificationBuilder(); } @@ -94,6 +123,12 @@ public String toString() { // TODO isDeprecatedの高速化 public boolean isDeprecated() { if (_deprecated == false) { + long intervalMillis = getExistsCheckIntervalMillis(); + long now = System.currentTimeMillis(); + if (intervalMillis > 0 && (now - _lastExistsCheckMillis) < intervalMillis) { + return false; + } + _lastExistsCheckMillis = now; _deprecated = _hasSource != isSourceExists(); if (_deprecated == false) { if (_hasSource == false) { @@ -117,6 +152,24 @@ public boolean isDeprecated() { @Override public void deprecate() { _deprecated = true; + _xpathInjectNodes = null; + } + + /** + * この spec の XPath injection ノードリストを返す。 + * まだ構築されていない場合は {@code null} を返す。 + */ + public List getCachedXpathInjectNodes() { + return _xpathInjectNodes; + } + + /** + * この spec の XPath injection ノードリストをセットする。 + * 複数スレッドが同時に呼んでも内容は等価なので、上書きは許容する。 + * リストは変更不可ラッパーで保護して格納する。 + */ + public void setCachedXpathInjectNodes(List nodes) { + _xpathInjectNodes = Collections.unmodifiableList(nodes); } public void build() { diff --git a/src-impl/org/seasar/mayaa/impl/engine/specification/xpath/XPathUtil.java b/src-impl/org/seasar/mayaa/impl/engine/specification/xpath/XPathUtil.java index e78b17a9..16111733 100644 --- a/src-impl/org/seasar/mayaa/impl/engine/specification/xpath/XPathUtil.java +++ b/src-impl/org/seasar/mayaa/impl/engine/specification/xpath/XPathUtil.java @@ -18,6 +18,10 @@ import java.util.Collections; import java.util.Iterator; import java.util.NoSuchElementException; +import java.util.concurrent.TimeUnit; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; import org.jaxen.Context; import org.jaxen.ContextSupport; @@ -47,6 +51,15 @@ private XPathUtil() { // no instantiation. } + /** XPath パターン文字列 → コンパイル済み Pattern のキャッシュ(スレッドセーフ)。 + * XPath 式はアプリ起動後に原則変わらないが、最大 10,000 エントリを上限とし + * メモリ圧迫を防ぐ。 */ + private static final Cache _patternCache = + Caffeine.newBuilder() + .maximumSize(10_000) + .expireAfterAccess(30, TimeUnit.MINUTES) + .build(); + public static boolean matches(SpecificationNode test, String xpathExpr, Namespace namespace) { if (StringUtil.isEmpty(xpathExpr)) { @@ -65,12 +78,16 @@ public static boolean matches(SpecificationNode test, SpecificationNavigator.getInstance()); Context context = new Context(support); try { - Pattern pattern = PatternParser.parse(xpathExpr); + Pattern pattern = _patternCache.get(xpathExpr, expr -> { + try { + return PatternParser.parse(expr); + } catch (SAXPathException e) { + throw new RuntimeException(e); + } + }); return pattern.matches(test, context); } catch (JaxenException e) { throw new RuntimeException(e); - } catch (SAXPathException e) { - throw new RuntimeException(e); } } diff --git a/src/test/java/org/seasar/mayaa/functional/engine/BuildPerformanceTest.java b/src/test/java/org/seasar/mayaa/functional/engine/BuildPerformanceTest.java new file mode 100644 index 00000000..3059f18e --- /dev/null +++ b/src/test/java/org/seasar/mayaa/functional/engine/BuildPerformanceTest.java @@ -0,0 +1,122 @@ +/* + * Copyright 2004-2012 the Seasar Foundation and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.seasar.mayaa.functional.engine; + +import java.io.IOException; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.seasar.mayaa.functional.EngineTestBase; +import org.seasar.mayaa.impl.engine.EngineImpl; +import org.seasar.mayaa.impl.provider.ProviderUtil; +import org.springframework.mock.web.MockHttpServletRequest; + +/** + * テンプレートビルド(injection フェーズ)の性能測定。 + * キャッシュ最適化の効果を確認するための簡易ベンチマーク。 + *

+ * 通常の CI では実行されない。{@code mvn test -P withPerformanceTest} で実行する。 + */ +@Tag("test.PerformanceTest") +public class BuildPerformanceTest extends EngineTestBase { + + private static final int WARMUP = 100; + private static final int ITERATIONS = 2000; + + /** + * XPath ベースの injection (XPathMatchesInjectionResolver) の繰り返しビルド時間を計測。 + */ + @Test + public void bench_inject_xpath() throws IOException { + final String TARGET = "/it-case/engine/inject_xpath/target.html"; + runBench("inject_xpath (XPath cache)", TARGET); + } + + /** + * ID ベースの injection (EqualsIDInjectionResolver) の繰り返しビルド時間を計測。 + */ + @Test + public void bench_inject_mayaa() throws IOException { + final String TARGET = "/it-case/engine/inject_mayaa/target.html"; + runBench("inject_mayaa (ID cache) ", TARGET); + } + + /** + * XPath ベースの injection — テンプレートのみ deprecate(.mayaa は再読み込みしない)。 + * injection フェーズ単体の性能を計測しやすい。 + */ + @Test + public void bench_inject_xpath_templateonly() throws IOException { + final String TARGET = "/it-case/engine/inject_xpath/target.html"; + runBench("inject_xpath (template-only) ", TARGET, false); + } + + /** + * ID ベースの injection — テンプレートのみ deprecate(.mayaa は再読み込みしない)。 + */ + @Test + public void bench_inject_mayaa_templateonly() throws IOException { + final String TARGET = "/it-case/engine/inject_mayaa/target.html"; + runBench("inject_mayaa (template-only)", TARGET, false); + } + + // ------------------------------------------------------------------ + + private void runBench(String label, String target) throws IOException { + runBench(label, target, true, false); + } + + private void runBench(String label, String target, boolean deprecateMayaa) throws IOException { + runBench(label, target, deprecateMayaa, false); + } + + private void runBench(String label, String target, boolean deprecateMayaa, boolean deprecateDefault) throws IOException { + EngineImpl engine = (EngineImpl) ProviderUtil.getEngine(); + + // ウォームアップ(JIT 最適化を安定させる) + for (int i = 0; i < WARMUP; i++) { + execAndDeprecate(engine, target, deprecateMayaa, deprecateDefault); + } + + // 計測 + long start = System.nanoTime(); + for (int i = 0; i < ITERATIONS; i++) { + execAndDeprecate(engine, target, deprecateMayaa, deprecateDefault); + } + long elapsed = System.nanoTime() - start; + + double totalMs = elapsed / 1_000_000.0; + double perMs = totalMs / ITERATIONS; + System.out.printf("[BENCH] %-38s %d iters total=%.1f ms per=%.3f ms%n", + label, ITERATIONS, totalMs, perMs); + } + + private void execAndDeprecate(EngineImpl engine, String path, boolean deprecateMayaa, boolean deprecateDefault) { + MockHttpServletRequest request = createRequest(path); + exec(request, null); + // キャッシュを無効化して毎回ビルドを走らせる + engine.deprecateSpecification(path); + if (deprecateMayaa) { + // .mayaa も無効化する(injection ルールの再読み込みを強制) + String mayaaPath = path.replaceAll("\\.[^.]+$", ".mayaa"); + engine.deprecateSpecification(mayaaPath); + } + if (deprecateDefault) { + // default.mayaa も無効化する(全スペック再構築を強制) + engine.deprecateSpecification("/default.mayaa"); + } + } +}