Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,11 @@ jmh {
jmhVersion = "1.37"
}

// Дерево зависимостей проекта даёт в jmh-архиве больше 65535 записей — нужен zip64.
tasks.named<Jar>("jmhJar") {
isZip64 = true
}

sentry {
org.set("1c-syntax")
projectName.set("bsl-language-server")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* This file is a part of BSL Language Server.
*
* Copyright (c) 2018-2026
* Alexey Sosnoviy <labotamy@gmail.com>, Nikita Fedkin <nixel2007@gmail.com> and contributors
*
* SPDX-License-Identifier: LGPL-3.0-or-later
*
* BSL Language Server 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.
*
* BSL Language Server 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 BSL Language Server.
*/
package com.github._1c_syntax.bsl.languageserver.types.registry;

import com.github._1c_syntax.bsl.languageserver.context.FileType;
import com.github._1c_syntax.bsl.languageserver.types.model.MemberDescriptor;
import com.github._1c_syntax.bsl.languageserver.types.model.TypeKind;
import com.github._1c_syntax.bsl.languageserver.types.model.TypeRef;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

/**
* Микробенчмарк горячего пути {@link TypeRegistry#getMembers} — попадания в memo.
* <p>
* Мерит цену пер-типового поколения, добавленного к проверке актуальности записи:
* {@link #realGetMembersHit} — реальный путь целиком, {@link #lookupEpochOnly} и
* {@link #lookupEpochAndGeneration} — изолированные модели проверки «до» и «после»
* на картах того же размера, их разница и есть накладной расход одного лишнего
* поиска в карте поколений.
* <p>
* Параметр {@code invalidatedTypes} задаёт число типов, которые реально
* инвалидировались (размер карты поколений): {@code 0} — типичный случай, когда
* правок не было и карта пуста, {@code 440} — все типы рабочей области были
* инвалидированы хотя бы раз (столько различных типов memo держит на cpm).
*/
@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MembersCacheLookup {

/** Столько различных типов держит memo при анализе cpm. */
private static final int TYPE_COUNT = 440;
private static final int MEMBERS_PER_TYPE = 20;

private TypeRegistry typeRegistry;
private TypeRef hotRef;

private final AtomicLong epoch = new AtomicLong();
private Map<Key, Cached> cache;
private Map<Key, Long> generation;
private Key hotKey;

@Param({"0", "440"})
int invalidatedTypes;

private record Key(TypeRef ref, FileType fileType) {
}

private record Cached(long epoch, long generation, List<MemberDescriptor> members) {
}

@Setup(Level.Trial)
public void setup() {
typeRegistry = new TypeRegistry(List.of(), new MemberMetadataIndex());
cache = new ConcurrentHashMap<>();
generation = new ConcurrentHashMap<>();

var refs = new ArrayList<TypeRef>(TYPE_COUNT);
for (var i = 0; i < TYPE_COUNT; i++) {
var ref = typeRegistry.intern(TypeKind.PLATFORM, "БенчТип" + i);
refs.add(ref);
var members = new ArrayList<MemberDescriptor>(MEMBERS_PER_TYPE);
for (var m = 0; m < MEMBERS_PER_TYPE; m++) {
members.add(MemberDescriptor.property("Член" + m, TypeRef.UNKNOWN, ""));
}
typeRegistry.registerMemberSource(ref, () -> members, FileType.BSL);
cache.put(new Key(ref, FileType.BSL), new Cached(0L, 0L, members));
}

// Инвалидируем заданную долю типов, чтобы карта поколений имела реалистичный размер,
// и снова прогреваем memo — замеряем именно попадание, а не пересборку.
for (var i = 0; i < invalidatedTypes; i++) {
var ref = refs.get(i);
typeRegistry.invalidateMembers(ref);
generation.put(new Key(ref, FileType.BSL), 1L);
}
for (var ref : refs) {
typeRegistry.getMembers(ref, FileType.BSL);
}

hotRef = refs.get(0);
hotKey = new Key(hotRef, FileType.BSL);
// приводим модельную запись к тому же состоянию, что и после инвалидации
cache.put(hotKey, new Cached(0L, generation.getOrDefault(hotKey, 0L), cache.get(hotKey).members()));
}

/** Реальный горячий путь: попадание в memo со сверкой эпохи и поколения. */
@Benchmark
@Fork(value = 2, warmups = 1)
@Warmup(time = 2, iterations = 3)
@Measurement(time = 2, iterations = 5)
public void realGetMembersHit(Blackhole bh) {
bh.consume(typeRegistry.getMembers(hotRef, FileType.BSL));
}

/** Модель проверки «до»: только эпоха. */
@Benchmark
@Fork(value = 2, warmups = 1)
@Warmup(time = 2, iterations = 3)
@Measurement(time = 2, iterations = 5)
public void lookupEpochOnly(Blackhole bh) {
var currentEpoch = epoch.get();
var cached = cache.get(hotKey);
if (cached != null && cached.epoch() == currentEpoch) {
bh.consume(cached.members());
}
}

/** Модель проверки «после»: эпоха плюс пер-типовое поколение. */
@Benchmark
@Fork(value = 2, warmups = 1)
@Warmup(time = 2, iterations = 3)
@Measurement(time = 2, iterations = 5)
public void lookupEpochAndGeneration(Blackhole bh) {
var currentEpoch = epoch.get();
var currentGeneration = generation.getOrDefault(hotKey, 0L);
var cached = cache.get(hotKey);
if (cached != null && cached.epoch() == currentEpoch && cached.generation() == currentGeneration) {
bh.consume(cached.members());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@
import org.springframework.stereotype.Component;

import java.net.URI;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -97,6 +99,41 @@ public void handleEvent(DocumentContextContentChangedEvent event) {
return;
}
register(documentContext);
invalidateMembersOfDocumentAndSubtypes(documentContext);
}

/**
* Точечно сбросить memo членов правленого документа и всех его наследников.
* <p>
* Member-source типа лениво читает символьное дерево своего документа, поэтому правка
* требует пересборки его членов. Наследники задеты транзитивно: источник унаследованных
* членов ({@code TypeRelations.inheritedMembers}) копирует к себе результат
* {@code getMembers} родителя, так что в memo наследника лежит снимок членов родителя —
* сброса одного лишь родителя недостаточно, снимок «протухает» на всю глубину иерархии.
* <p>
* Обход идёт по прямым наследникам ({@code &Расширяет}); интерфейсы ({@code &Реализует})
* членов не приносят, поэтому реализаторов обходить не нужно. Повторные посещения
* отсекаются по URI — это же защищает от циклов в объявлениях наследования.
*
* @param documentContext правленый {@code .os}-документ.
*/
private void invalidateMembersOfDocumentAndSubtypes(DocumentContext documentContext) {
var visited = new HashSet<URI>();
var queue = new ArrayDeque<DocumentContext>();
queue.add(documentContext);
while (!queue.isEmpty()) {
var current = queue.poll();
if (!visited.add(current.getUri())) {
continue;
}
var names = registeredByUri.get(current.getUri());
if (names != null) {
for (var name : names) {
typeRegistry.resolve(name, FileType.OS).ifPresent(typeRegistry::invalidateMembers);
}
}
queue.addAll(typeRelations.subtypes(current));
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,10 @@
var prev = registeredByUri.put(documentContext.getUri(), ref);
globalScopeProvider.indexModuleType(documentContext.getUri(), ref);
if (prev != null && prev.equals(ref)) {
// тот же URI/тип — источник уже зарегистрирован, AST подхватится автоматически
// источник уже зарегистрирован, но содержимое изменилось (rebuild) — его member-source
// лениво читает символьное дерево, поэтому точечно сбрасываем memo членов только этого
// типа; кэши прочих типов остаются валидными (без сдвига глобальной эпохи).
typeRegistry.invalidateMembers(ref);
return;
}

Expand Down Expand Up @@ -160,6 +163,12 @@
documentContext.getSymbolTree().getModule());

if (prev != null && prev.equals(ref)) {
// содержимое изменилось (rebuild): точечно пересобрать memo членов самого модуля
// и члена GLOBAL_CONTEXT (в него вошёл обновлённый symbol-источник модуля) — без
// сдвига глобальной эпохи членов. Name-индекс глобальной области отдельно сбрасывать
// не нужно: он кэширован по набору-источнику и пересоберётся сам, увидев новый набор.
typeRegistry.invalidateMembers(ref);
typeRegistry.invalidateMembers(TypeRegistry.GLOBAL_CONTEXT);
return;
}

Expand All @@ -179,11 +188,11 @@
documentContext.getSymbolTree().getMethods().stream()
.filter(MethodSymbol::isExport)
.map(this::toMethodMember)
.forEach(members::add);

Check warning on line 191 in src/main/java/com/github/_1c_syntax/bsl/languageserver/types/registry/ConfigurationModuleMembersProvider.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "collect(Collectors.toList())" instead of "forEach(members::add)".

See more on https://sonarcloud.io/project/issues?id=1c-syntax_bsl-language-server&issues=AZ9sCAlmTAot3AzgAs5K&open=AZ9sCAlmTAot3AzgAs5K&pullRequest=4280
documentContext.getSymbolTree().getVariables().stream()
.filter(VariableSymbol::isExport)
.map(this::toVariableMember)
.forEach(members::add);

Check warning on line 195 in src/main/java/com/github/_1c_syntax/bsl/languageserver/types/registry/ConfigurationModuleMembersProvider.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "collect(Collectors.toList())" instead of "forEach(members::add)".

See more on https://sonarcloud.io/project/issues?id=1c-syntax_bsl-language-server&issues=AZ9sCAlmTAot3AzgAs5L&open=AZ9sCAlmTAot3AzgAs5L&pullRequest=4280
return members;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Supplier;
Expand Down Expand Up @@ -109,7 +108,7 @@
* {@code GlobalScopeProvider → TypeRegistry}, без цикла.
*/
private final TypeRegistry typeRegistry;
/** Эпоха-кэшированный name-индекс членов GLOBAL_CONTEXT (см. {@link #globalMember}). */
/** Name-индекс членов GLOBAL_CONTEXT, кэшированный по наборам-источникам (см. {@link #globalMember}). */
private final AtomicReference<GlobalIndex> globalIndexRef = new AtomicReference<>();
/**
* URI документа-модуля → его тип-значение (обратный индекс к name-keyed записям).
Expand Down Expand Up @@ -167,9 +166,10 @@
* Резолв безпрефиксного имени в член глобальной области — синтетического типа
* {@link TypeRegistry#GLOBAL_CONTEXT} (глобальная функция-метод либо глобальное
* свойство: перечисление, менеджер коллекции, общий/library-модуль). Быстрый
* lookup по name-индексу, пересобираемому при смене эпохи членов
* ({@link TypeRegistry#membersEpoch()}). Единая абстракция доступа
* к глобальной области; {@link TypeRegistry} остаётся хранилищем типов.
* lookup по name-индексу, который пересобирается, когда {@code getMembers} отдаёт
* новые наборы членов {@code GLOBAL_CONTEXT} (то есть после любой их инвалидации).
* Единая абстракция доступа к глобальной области; {@link TypeRegistry} остаётся
* хранилищем типов.
*
* @param name имя (регистронезависимо, ru/en).
* @param fileType язык файла-потребителя.
Expand All @@ -179,13 +179,24 @@
if (name == null || name.isBlank()) {
return Optional.empty();
}
var epoch = typeRegistry.membersEpoch();
// Индекс — производная от членов GLOBAL_CONTEXT, поэтому его актуальность определяется
// самими наборами-источниками: getMembers отдаёт тот же экземпляр списка, пока memo живо,
// и новый — после любой инвалидации (эпоха или пер-типовое поколение). Отдельный счётчик
// поколения индекса не нужен: сверки идентичности источников достаточно, и она же
// отбрасывает индекс, собранный параллельно из устаревших членов.
var bslSource = typeRegistry.getMembers(TypeRegistry.GLOBAL_CONTEXT, FileType.BSL);
var osSource = typeRegistry.getMembers(TypeRegistry.GLOBAL_CONTEXT, FileType.OS);
var index = globalIndexRef.get();
if (index == null || index.epoch() != epoch) {
index = new GlobalIndex(epoch, Map.of(
FileType.BSL, globalNameIndex(FileType.BSL),
FileType.OS, globalNameIndex(FileType.OS)));
globalIndexRef.set(index);
if (index == null || index.bslSource() != bslSource || index.osSource() != osSource) {

Check warning on line 190 in src/main/java/com/github/_1c_syntax/bsl/languageserver/types/registry/GlobalScopeProvider.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the "equals" method if value comparison was intended.

See more on https://sonarcloud.io/project/issues?id=1c-syntax_bsl-language-server&issues=AZ-Fk_ebqf24P60kk4Rf&open=AZ-Fk_ebqf24P60kk4Rf&pullRequest=4280

Check warning on line 190 in src/main/java/com/github/_1c_syntax/bsl/languageserver/types/registry/GlobalScopeProvider.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the "equals" method if value comparison was intended.

See more on https://sonarcloud.io/project/issues?id=1c-syntax_bsl-language-server&issues=AZ-Fk_ebqf24P60kk4Re&open=AZ-Fk_ebqf24P60kk4Re&pullRequest=4280
var rebuilt = new GlobalIndex(bslSource, osSource, Map.of(
FileType.BSL, globalNameIndex(bslSource),
FileType.OS, globalNameIndex(osSource)));
// CAS, а не set: параллельный поток мог опубликовать индекс по более свежим наборам,
// и затирать его своим не нужно — иначе следующее чтение увидит рассинхрон и зря
// пересоберёт индекс. Собранный здесь экземпляр всё равно валиден для этого вызова:
// он построен ровно из тех наборов, которые мы прочитали выше.
globalIndexRef.compareAndSet(index, rebuilt);
index = rebuilt;
}
return Optional.ofNullable(index.byName().get(fileType).get(name.toLowerCase(Locale.ROOT)));
}
Expand Down Expand Up @@ -250,9 +261,10 @@
return result;
}

private Map<String, MemberDescriptor> globalNameIndex(FileType fileType) {
var map = new HashMap<String, MemberDescriptor>();
for (var member : typeRegistry.getMembers(TypeRegistry.GLOBAL_CONTEXT, fileType)) {
private Map<String, MemberDescriptor> globalNameIndex(Collection<MemberDescriptor> members) {

Check warning on line 264 in src/main/java/com/github/_1c_syntax/bsl/languageserver/types/registry/GlobalScopeProvider.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make "globalNameIndex" a "static" method.

See more on https://sonarcloud.io/project/issues?id=1c-syntax_bsl-language-server&issues=AZ-Fk_ebqf24P60kk4Rg&open=AZ-Fk_ebqf24P60kk4Rg&pullRequest=4280
// до двух записей на член (ru и en) — задаём ёмкость сразу, чтобы не рехэшировать
var map = HashMap.<String, MemberDescriptor>newHashMap(members.size() * 2);

Check warning on line 266 in src/main/java/com/github/_1c_syntax/bsl/languageserver/types/registry/GlobalScopeProvider.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this magic number 2 to a well-named constant, and use the constant instead.

See more on https://sonarcloud.io/project/issues?id=1c-syntax_bsl-language-server&issues=AZ-FuWLhlM3PEnpiQ9oa&open=AZ-FuWLhlM3PEnpiQ9oa&pullRequest=4280
for (var member : members) {
var ru = member.bilingualName().ru();
var en = member.bilingualName().en();
if (!ru.isBlank()) {
Expand All @@ -265,8 +277,13 @@
return map;
}

/** Эпоха-кэшированный индекс имён членов GLOBAL_CONTEXT в разрезе языка. */
private record GlobalIndex(long epoch, Map<FileType, Map<String, MemberDescriptor>> byName) {
/**
* Индекс имён членов GLOBAL_CONTEXT в разрезе языка вместе с наборами-источниками,
* из которых он собран: сверка их идентичности и есть критерий актуальности индекса.
*/
private record GlobalIndex(Collection<MemberDescriptor> bslSource,
Collection<MemberDescriptor> osSource,
Map<FileType, Map<String, MemberDescriptor>> byName) {
}

/**
Expand Down
Loading
Loading