From 654da411352dda1acde9bf801de22c249d654c27 Mon Sep 17 00:00:00 2001 From: MW Date: Wed, 20 May 2026 10:51:37 +0900 Subject: [PATCH 1/2] =?UTF-8?q?perf:=20XPath=E3=83=91=E3=82=BF=E3=83=BC?= =?UTF-8?q?=E3=83=B3=E3=82=AD=E3=83=A3=E3=83=83=E3=82=B7=E3=83=A5=E3=81=A8?= =?UTF-8?q?=20XPathMatchesInjectionResolver=20=E3=81=AE=20per-spec=20?= =?UTF-8?q?=E3=82=AD=E3=83=A3=E3=83=83=E3=82=B7=E3=83=A5=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - XPathUtil._patternCache (ConcurrentHashMap): PatternParser.parse() を毎回呼び出していたのを 初回のみパースしてキャッシュするよう変更。XPath 式はアプリ起動後に固定のため安全。 - XPathMatchesInjectionResolver: spec インスタンスへの _xpathInjectNodes キャッシュ導入 - 毎ノードの spec 階層走査を削減(100ノード → 1回の構築) - per-spec キャッシュにより複数スレッドで共用。deprecate() でクリア。 - XPATH_NAMESPACE を static 定数化(毎回 createNamespace() 廃止) - SpecificationImpl: isDeprecated() の存在チェックを 500ms 間隔に throttle - getExistsCheckIntervalMillis() で設定値をインスタンスフィールドに遅延キャッシュし 毎回の EngineSetting 参照と Long.parseLong() オーバーヘッドを除去 ベンチマーク結果 (BuildPerformanceTest、2000 iters): inject_xpath (template-only): 0.592ms → 0.365ms (-38%) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../org/seasar/mayaa/impl/CONST_IMPL.java | 1 + .../XPathMatchesInjectionResolver.java | 68 +++++++--- .../specification/SpecificationImpl.java | 51 ++++++++ .../engine/specification/xpath/XPathUtil.java | 15 ++- .../engine/BuildPerformanceTest.java | 118 ++++++++++++++++++ 5 files changed, 230 insertions(+), 23 deletions(-) create mode 100644 src/test/java/org/seasar/mayaa/functional/engine/BuildPerformanceTest.java 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..4bb75c76 100644 --- a/src-impl/org/seasar/mayaa/impl/engine/specification/SpecificationImpl.java +++ b/src-impl/org/seasar/mayaa/impl/engine/specification/SpecificationImpl.java @@ -17,8 +17,11 @@ 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; @@ -48,6 +51,15 @@ public class SpecificationImpl extends ParameterAwareImpl implements Specificati private boolean _hasSource; private 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 +69,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 +122,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 +151,23 @@ 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 = 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..780ec9bb 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,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.NoSuchElementException; +import java.util.concurrent.ConcurrentHashMap; import org.jaxen.Context; import org.jaxen.ContextSupport; @@ -47,6 +48,10 @@ private XPathUtil() { // no instantiation. } + /** XPath パターン文字列 → コンパイル済み Pattern のキャッシュ(スレッドセーフ)。 */ + private static final ConcurrentHashMap _patternCache = + new ConcurrentHashMap<>(); + public static boolean matches(SpecificationNode test, String xpathExpr, Namespace namespace) { if (StringUtil.isEmpty(xpathExpr)) { @@ -65,12 +70,16 @@ public static boolean matches(SpecificationNode test, SpecificationNavigator.getInstance()); Context context = new Context(support); try { - Pattern pattern = PatternParser.parse(xpathExpr); + Pattern pattern = _patternCache.computeIfAbsent(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..d52792e8 --- /dev/null +++ b/src/test/java/org/seasar/mayaa/functional/engine/BuildPerformanceTest.java @@ -0,0 +1,118 @@ +/* + * 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.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 フェーズ)の性能測定。 + * キャッシュ最適化の効果を確認するための簡易ベンチマーク。 + */ +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"); + } + } +} From b099873944aa1dc7e51b61d124839d4871152bf2 Mon Sep 17 00:00:00 2001 From: MW Date: Wed, 20 May 2026 11:28:04 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20Copilot=E3=83=AC=E3=83=93=E3=83=A5?= =?UTF-8?q?=E3=83=BC=E6=8C=87=E6=91=98=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SpecificationImpl._deprecated を volatile 化 isDeprecated() のスロットリングでは _lastExistsCheckMillis(volatile)を 更新後に _deprecated(非volatile)を更新していたため、他スレッドが新しい _lastExistsCheckMillis を見て早期 return しつつ古い _deprecated を読む 恐れがあった。volatile 化により可視性を保証する。 - setCachedXpathInjectNodes() で Collections.unmodifiableList() でラップ getter が返すリストが外部から変更されないよう保護する。 - XPathUtil._patternCache を Caffeine キャッシュに変更(max 10,000、30分 TTI) ConcurrentHashMap の無制限成長を防ぐ。XPath 式が動的に生成される環境でも メモリを回収できるようにする。 - BuildPerformanceTest に @Tag("test.PerformanceTest") を付与 pom.xml の testExcludedGroups 設定により通常 CI では除外される。 実行するには mvn test -P withPerformanceTest を使う。 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../specification/SpecificationImpl.java | 6 ++++-- .../engine/specification/xpath/XPathUtil.java | 18 +++++++++++++----- .../engine/BuildPerformanceTest.java | 4 ++++ 3 files changed, 21 insertions(+), 7 deletions(-) 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 4bb75c76..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,6 +15,7 @@ */ package org.seasar.mayaa.impl.engine.specification; +import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.List; @@ -49,7 +50,7 @@ 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; @@ -165,9 +166,10 @@ public List getCachedXpathInjectNodes() { /** * この spec の XPath injection ノードリストをセットする。 * 複数スレッドが同時に呼んでも内容は等価なので、上書きは許容する。 + * リストは変更不可ラッパーで保護して格納する。 */ public void setCachedXpathInjectNodes(List nodes) { - _xpathInjectNodes = 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 780ec9bb..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,7 +18,10 @@ import java.util.Collections; import java.util.Iterator; import java.util.NoSuchElementException; -import java.util.concurrent.ConcurrentHashMap; +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; @@ -48,9 +51,14 @@ private XPathUtil() { // no instantiation. } - /** XPath パターン文字列 → コンパイル済み Pattern のキャッシュ(スレッドセーフ)。 */ - private static final ConcurrentHashMap _patternCache = - new ConcurrentHashMap<>(); + /** 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) { @@ -70,7 +78,7 @@ public static boolean matches(SpecificationNode test, SpecificationNavigator.getInstance()); Context context = new Context(support); try { - Pattern pattern = _patternCache.computeIfAbsent(xpathExpr, expr -> { + Pattern pattern = _patternCache.get(xpathExpr, expr -> { try { return PatternParser.parse(expr); } catch (SAXPathException 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 index d52792e8..3059f18e 100644 --- a/src/test/java/org/seasar/mayaa/functional/engine/BuildPerformanceTest.java +++ b/src/test/java/org/seasar/mayaa/functional/engine/BuildPerformanceTest.java @@ -17,6 +17,7 @@ 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; @@ -26,7 +27,10 @@ /** * テンプレートビルド(injection フェーズ)の性能測定。 * キャッシュ最適化の効果を確認するための簡易ベンチマーク。 + *

+ * 通常の CI では実行されない。{@code mvn test -P withPerformanceTest} で実行する。 */ +@Tag("test.PerformanceTest") public class BuildPerformanceTest extends EngineTestBase { private static final int WARMUP = 100;