From e108377db3164603e4900353061200de5ecae77e Mon Sep 17 00:00:00 2001 From: Nikita Fedkin Date: Wed, 15 Jul 2026 20:32:47 +0200 Subject: [PATCH 1/2] perf(reader): lock-free QNameMap to remove per-element synchronized-map contention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Профилирование чтения показало, что QNameMap.getJavaClassName вызывается на каждый XML-элемент и делает get по Collections.synchronizedMap. Метаданные читаются параллельно (parallelStream), поэтому все потоки сериализуются на мониторе одной общей карты — по профилю ~5% CPU уходит в SynchronizedMap.get. Вводим ConcurrentQNameMap с картами на ConcurrentHashMap (чтение без блокировок). Отображения регистрируются один раз до чтения, во время чтения карты только читаются. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01HLvkKQCwBjTHwuZ4EGBQK6 --- .../common/xstream/ConcurrentQNameMap.java | 61 +++++++++++++++++++ .../bsl/reader/designer/DesignerReader.java | 4 +- 2 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/github/_1c_syntax/bsl/reader/common/xstream/ConcurrentQNameMap.java diff --git a/src/main/java/com/github/_1c_syntax/bsl/reader/common/xstream/ConcurrentQNameMap.java b/src/main/java/com/github/_1c_syntax/bsl/reader/common/xstream/ConcurrentQNameMap.java new file mode 100644 index 000000000..6f4f7a927 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/reader/common/xstream/ConcurrentQNameMap.java @@ -0,0 +1,61 @@ +/* + * This file is a part of MDClasses. + * + * Copyright (c) 2019 - 2026 + * Tymko Oleg , Maximov Valery and contributors + * + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * MDClasses is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * MDClasses is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with MDClasses. + */ +package com.github._1c_syntax.bsl.reader.common.xstream; + +import com.thoughtworks.xstream.io.xml.QNameMap; + +import javax.xml.namespace.QName; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * {@link QNameMap} с потокобезопасными картами без блокировок на чтении. + *

+ * Штатный {@link QNameMap} хранит соответствия в {@code Collections.synchronizedMap}, а + * {@link #getJavaClassName(QName)} вызывается на каждый XML-элемент. При параллельном чтении + * (метаданные читаются {@code parallelStream}) все потоки сериализуются на мониторе этой общей + * карты — по профилю это заметная доля CPU. Здесь карты — {@link ConcurrentHashMap}, чтение + * которых свободно от блокировок; отображения регистрируются один раз до начала чтения. + */ +public class ConcurrentQNameMap extends QNameMap { + + private final Map qnameToJava = new ConcurrentHashMap<>(); + private final Map javaToQName = new ConcurrentHashMap<>(); + + @Override + public String getJavaClassName(QName qname) { + var answer = qnameToJava.get(qname); + return answer != null ? answer : qname.getLocalPart(); + } + + @Override + public QName getQName(String javaClassName) { + var answer = javaToQName.get(javaClassName); + return answer != null ? answer : new QName(getDefaultNamespace(), javaClassName, getDefaultPrefix()); + } + + @Override + public void registerMapping(QName qname, String javaClassName) { + javaToQName.put(javaClassName, qname); + qnameToJava.put(qname, javaClassName); + } +} diff --git a/src/main/java/com/github/_1c_syntax/bsl/reader/designer/DesignerReader.java b/src/main/java/com/github/_1c_syntax/bsl/reader/designer/DesignerReader.java index a3ee85605..70eec7076 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/reader/designer/DesignerReader.java +++ b/src/main/java/com/github/_1c_syntax/bsl/reader/designer/DesignerReader.java @@ -59,6 +59,7 @@ import com.github._1c_syntax.bsl.reader.MDReader; import com.github._1c_syntax.bsl.reader.common.context.AbstractReaderContext; import com.github._1c_syntax.bsl.reader.common.converter.DesignerRootWrapper; +import com.github._1c_syntax.bsl.reader.common.xstream.ConcurrentQNameMap; import com.github._1c_syntax.bsl.reader.common.xstream.ExtendXStream; import com.github._1c_syntax.bsl.reader.designer.converter.DesignerConverter; import com.github._1c_syntax.bsl.reader.designer.converter.Unmarshaller; @@ -71,7 +72,6 @@ import com.thoughtworks.xstream.core.ClassLoaderReference; import com.thoughtworks.xstream.core.util.CompositeClassLoader; import com.thoughtworks.xstream.io.HierarchicalStreamReader; -import com.thoughtworks.xstream.io.xml.QNameMap; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FilenameUtils; @@ -239,7 +239,7 @@ public void unmarshal(HierarchicalStreamReader reader, } private ExtendXStream createXMLMapper() { - var qNameMap = new QNameMap(); + var qNameMap = new ConcurrentQNameMap(); qNameMap.registerMapping(new QName("http://v8.1c.ru/8.3/xcf/logform", "Form"), ManagedFormData.class); var classLoaderReference = new ClassLoaderReference(new CompositeClassLoader()); From 1fef2fa5c584bcea4a922737bb484e6f6f37539c Mon Sep 17 00:00:00 2001 From: Nikita Fedkin Date: Wed, 15 Jul 2026 21:18:39 +0200 Subject: [PATCH 2/2] fix(reader): synchronized registerMapping + ConcurrentQNameMap tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - registerMapping помечен synchronized, чтобы соответствовать контракту родителя QNameMap (Sonar S3551); метод вызывается только на этапе настройки, на горячий путь чтения не влияет. - Добавлен ConcurrentQNameMapTest (регистрация/резолв, фолбэки) — покрытие нового класса. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01HLvkKQCwBjTHwuZ4EGBQK6 --- .../common/xstream/ConcurrentQNameMap.java | 2 +- .../xstream/ConcurrentQNameMapTest.java | 72 +++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/github/_1c_syntax/bsl/reader/common/xstream/ConcurrentQNameMapTest.java diff --git a/src/main/java/com/github/_1c_syntax/bsl/reader/common/xstream/ConcurrentQNameMap.java b/src/main/java/com/github/_1c_syntax/bsl/reader/common/xstream/ConcurrentQNameMap.java index 6f4f7a927..5d34c0270 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/reader/common/xstream/ConcurrentQNameMap.java +++ b/src/main/java/com/github/_1c_syntax/bsl/reader/common/xstream/ConcurrentQNameMap.java @@ -54,7 +54,7 @@ public QName getQName(String javaClassName) { } @Override - public void registerMapping(QName qname, String javaClassName) { + public synchronized void registerMapping(QName qname, String javaClassName) { javaToQName.put(javaClassName, qname); qnameToJava.put(qname, javaClassName); } diff --git a/src/test/java/com/github/_1c_syntax/bsl/reader/common/xstream/ConcurrentQNameMapTest.java b/src/test/java/com/github/_1c_syntax/bsl/reader/common/xstream/ConcurrentQNameMapTest.java new file mode 100644 index 000000000..f43ca3e2e --- /dev/null +++ b/src/test/java/com/github/_1c_syntax/bsl/reader/common/xstream/ConcurrentQNameMapTest.java @@ -0,0 +1,72 @@ +/* + * This file is a part of MDClasses. + * + * Copyright (c) 2019 - 2026 + * Tymko Oleg , Maximov Valery and contributors + * + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * MDClasses is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * MDClasses is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with MDClasses. + */ +package com.github._1c_syntax.bsl.reader.common.xstream; + +import org.junit.jupiter.api.Test; + +import javax.xml.namespace.QName; + +import static org.assertj.core.api.Assertions.assertThat; + +class ConcurrentQNameMapTest { + + private static final QName FORM_QNAME = new QName("http://v8.1c.ru/8.3/xcf/logform", "Form"); + private static final String FORM_CLASS = "com.github._1c_syntax.bsl.mdo.storage.form.ManagedFormData"; + + @Test + void mappedQNameResolvesToRegisteredClassAndBack() { + // given + var map = new ConcurrentQNameMap(); + + // when + map.registerMapping(FORM_QNAME, FORM_CLASS); + + // then + assertThat(map.getJavaClassName(FORM_QNAME)).isEqualTo(FORM_CLASS); + assertThat(map.getQName(FORM_CLASS)).isEqualTo(FORM_QNAME); + } + + @Test + void unmappedQNameFallsBackToLocalPart() { + // given + var map = new ConcurrentQNameMap(); + + // when + var javaClassName = map.getJavaClassName(new QName("http://ns", "Catalog")); + + // then + assertThat(javaClassName).isEqualTo("Catalog"); + } + + @Test + void unmappedClassNameFallsBackToDefaultQName() { + // given + var map = new ConcurrentQNameMap(); + + // when + var qName = map.getQName("Catalog"); + + // then + assertThat(qName.getLocalPart()).isEqualTo("Catalog"); + assertThat(qName.getNamespaceURI()).isEqualTo(map.getDefaultNamespace()); + } +}