From 01fd25ab9b20c879c58638f42e24ee7a76fdb67a Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Tue, 3 Nov 2026 17:37:35 +0800 Subject: [PATCH] perf(java): keep the configuration snapshot across editor activity The configuration index was invalidated by every Java element event, including POST_RECONCILE, and its working-copy fingerprint covered every open editor. Opening the navigation target, closing a tab, or saving application.yml was enough to make the next Find Usages or YAML key navigation rebuild the whole project index. Invalidation now follows what the index depends on: - POST_CHANGE deltas only, ignoring working-copy open/close, AST-only flags and non-Java resource content on projects, roots and packages (configuration files are parsed by the client, metadata files are stamp-checked). - The fingerprint covers working copies with unsaved changes only. Each snapshot reports a generation so the real-server harness can assert that opening/closing editors, a full build and a saved edit keep it warm, while a source added on disk still invalidates it. Bundle 1.2.1 (installed as java-usages-1.2.1.jar so OSGi does not reuse the cached 1.2.0), reproducible SHA-256 cbae3e3a0bcf2818b4cc9d98998bd9b81f92e1ae7803dc757134485f9776a4e1. --- crates/ide-sdk/src/java.rs | 2 +- plugins/java-usages/CONFIGURATION.md | 11 ++++--- plugins/java-usages/MANIFEST.MF | 2 +- plugins/java-usages/README.md | 5 ++- plugins/java-usages/openidea-java-usages.jar | Bin 82033 -> 83078 bytes .../configuration/ConfigurationCommand.java | 27 +++++++++++++--- plugins/java-usages/tests/configuration.py | 29 +++++++++++++++++- 7 files changed, 63 insertions(+), 13 deletions(-) diff --git a/crates/ide-sdk/src/java.rs b/crates/ide-sdk/src/java.rs index 9120b0e5..623a8705 100644 --- a/crates/ide-sdk/src/java.rs +++ b/crates/ide-sdk/src/java.rs @@ -1173,7 +1173,7 @@ mod kotlin_bridge_bundle_tests { /// Installs the app-owned JDT LS command bundle, falling back to ordinary LSP on IO failure. pub fn usages_bundle(data_dir: &Path) -> Option { - let extension = data_dir.join("java-usages-1.2.0.jar"); + let extension = data_dir.join("java-usages-1.2.1.jar"); let bytes = include_bytes!("../../../plugins/java-usages/openidea-java-usages.jar"); if std::fs::read(&extension).is_ok_and(|old| old == bytes) { clean_usages_bundles(data_dir, &extension); diff --git a/plugins/java-usages/CONFIGURATION.md b/plugins/java-usages/CONFIGURATION.md index a696e1cd..f0965bcd 100644 --- a/plugins/java-usages/CONFIGURATION.md +++ b/plugins/java-usages/CONFIGURATION.md @@ -57,10 +57,13 @@ ordinary typing, code-lens probes, bulk scoped reference analysis, and Java Go t retained, with a shared 100,000-association retention cap. Records contain property names, element handles and source locations; ASTs and bindings are discarded after indexing. A lexical prefilter avoids AST construction for ordinary Java units, but every accepted association is resolved -semantically. Warm requests reuse the snapshot. Java model/reconcile changes -invalidate it conservatively, including dependent-type changes; open working-copy -content fingerprints also prevent stale answers before reconciliation. This is -**snapshot invalidation**, not a fully incremental Java semantic index. +semantically. Warm requests reuse the snapshot. Java model changes (sources on +disk, classpath, archives) invalidate it conservatively, including dependent-type +changes; a fingerprint of working copies with unsaved changes covers edits before +they are saved. Opening or closing an editor, reconciling, building, and non-Java +resource changes (configuration files are parsed by the client; metadata files are +checked by timestamp) keep it. This is **snapshot invalidation**, not a fully +incremental Java semantic index. Metadata/JAR timestamps are checked before reuse. The client caches parsed disk configuration files by modification time and size (at most 100,000 cached keys), overlays open buffers, and diff --git a/plugins/java-usages/MANIFEST.MF b/plugins/java-usages/MANIFEST.MF index 7ec5b7f3..2d5796c3 100644 --- a/plugins/java-usages/MANIFEST.MF +++ b/plugins/java-usages/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: OpenIDEA Java usages Bundle-SymbolicName: ai.openidea.usages;singleton:=true -Bundle-Version: 1.2.0 +Bundle-Version: 1.2.1 Bundle-RequiredExecutionEnvironment: JavaSE-21 Require-Bundle: org.eclipse.jdt.ls.core,org.eclipse.jdt.core, org.eclipse.jdt.core.manipulation,org.eclipse.core.runtime, diff --git a/plugins/java-usages/README.md b/plugins/java-usages/README.md index 6f820321..819fd2b3 100644 --- a/plugins/java-usages/README.md +++ b/plugins/java-usages/README.md @@ -57,7 +57,7 @@ is not a claim of full IntelliJ feature parity. ## Rebuild and delivery The checked-in jar is embedded by ide-sdk `include_bytes!`, installed as -`java-usages-1.2.0.jar`, and added alongside existing initialization bundles. +`java-usages-1.2.1.jar`, and added alongside existing initialization bundles. Users need no compiler, Maven or Python. Manifest/install version must change when replacing the bundle because OSGi caches bundles. @@ -70,6 +70,9 @@ and compare SHA-256. The prior 1.1.1 output was `1e569939cba22c7db6e2c9b53de2a4c10ccaabe762a51427119a04af3d974ae4`. Version 1.2.0 adds the configuration command; its reproducible SHA-256 is `9140f1d2ecf27eadfe66b40a17daa18a08a65927ebef79456ba55d486ce5eeba`. +Version 1.2.1 keeps the configuration snapshot across editor open/close, saves of +unrelated content and non-Java resource changes; its reproducible SHA-256 is +`cbae3e3a0bcf2818b4cc9d98998bd9b81f92e1ae7803dc757134485f9776a4e1`. ```sh export PATH=/path/to/jdk25/bin:$PATH diff --git a/plugins/java-usages/openidea-java-usages.jar b/plugins/java-usages/openidea-java-usages.jar index 3f521b3a3218b8f52fc4fa0808705773eec01462..bedb5a1a53ae670e988812e2dcce9c0bb860d340 100644 GIT binary patch delta 14972 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 b$>HwPRqkNgaP+%wXmH|iPIXlb(`@*ELhnbh delta 13900 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 diff --git a/plugins/java-usages/src/ai/openidea/configuration/ConfigurationCommand.java b/plugins/java-usages/src/ai/openidea/configuration/ConfigurationCommand.java index 39fce3a9..fdf037bb 100644 --- a/plugins/java-usages/src/ai/openidea/configuration/ConfigurationCommand.java +++ b/plugins/java-usages/src/ai/openidea/configuration/ConfigurationCommand.java @@ -27,9 +27,23 @@ public final class ConfigurationCommand implements IDelegateCommandHandler { private static final String VALUE = "org.springframework.beans.factory.annotation.Value"; private static final String RESOLVER = "org.springframework.core.env.PropertyResolver"; private static final int MAX_UNITS = 20_000, MAX_ENTRIES = 100_000, MAX_BYTES = 2 * 1024 * 1024; - private static final AtomicLong REVISION = new AtomicLong(); + private static final AtomicLong REVISION = new AtomicLong(), GENERATION = new AtomicLong(); private static final Map CACHE = new LinkedHashMap<>(); - static { JavaCore.addElementChangedListener(event -> REVISION.incrementAndGet(), ElementChangedEvent.POST_CHANGE | ElementChangedEvent.POST_RECONCILE); } + /** Unsaved edits are covered by the working-copy digest, so reconciles never + * invalidate. Opening/closing a working copy (F_PRIMARY_WORKING_COPY) and + * non-Java resources, reported as F_CONTENT on a project, root or package + * (configuration files are parsed by the client; metadata files carry their + * own stamps), do not change the index either. */ + private static final int IGNORED_FLAGS = IJavaElementDelta.F_CHILDREN | IJavaElementDelta.F_PRIMARY_WORKING_COPY + | IJavaElementDelta.F_AST_AFFECTED | IJavaElementDelta.F_FINE_GRAINED; + static { JavaCore.addElementChangedListener(event -> { if (affects(event.getDelta())) REVISION.incrementAndGet(); }, ElementChangedEvent.POST_CHANGE); } + static boolean affects(IJavaElementDelta delta) { + int flags = delta.getFlags() & ~IGNORED_FLAGS; + if (delta.getElement().getElementType() < IJavaElement.COMPILATION_UNIT) flags &= ~IJavaElementDelta.F_CONTENT; + if (delta.getKind() != IJavaElementDelta.CHANGED || flags != 0) return true; + for (IJavaElementDelta child : delta.getAffectedChildren()) if (affects(child)) return true; + return false; + } private record Entry(String name, String handle, Location location, boolean relaxed) {} private record FileStamp(java.nio.file.attribute.FileTime modified, long size) {} @@ -38,7 +52,7 @@ public final class ConfigurationCommand implements IDelegateCommandHandler { catch (java.io.IOException e) { return new FileStamp(null, -1); } } private record Index(long revision, String workingCopies, List entries, List roots, - Map files, boolean incomplete, int parsed) {} + Map files, boolean incomplete, int parsed, long generation) {} @Override public Object executeCommand(String command, List args, IProgressMonitor monitor) throws Exception { @@ -108,7 +122,7 @@ public final class ConfigurationCommand implements IDelegateCommandHandler { } return Map.of("locations", locations, "keys", keys, "roots", index.roots, "incomplete", index.incomplete || locations.size() >= 2000, - "parsedUnits", index.parsed, "revision", index.revision); + "parsedUnits", index.parsed, "revision", index.revision, "generation", index.generation); } private static boolean sameUri(String a, String b) { @@ -156,9 +170,12 @@ public final class ConfigurationCommand implements IDelegateCommandHandler { for (ICompilationUnit copy : JavaCore.getWorkingCopies(null)) copies.put(copy.getPrimary().getHandleIdentifier(), copy); for (ICompilationUnit copy : JavaCore.getWorkingCopies(JavaLanguageServerPlugin.getInstance().getWorkingCopyOwner())) copies.put(copy.getPrimary().getHandleIdentifier(), copy); + // Only unsaved contents differ from what the Java model reports for disk, + // so merely opening or closing an editor keeps the snapshot. var digest = java.security.MessageDigest.getInstance("SHA-256"); for (var entry : copies.entrySet()) { check(monitor); + if (!entry.getValue().hasUnsavedChanges()) continue; digest.update(entry.getKey().getBytes(java.nio.charset.StandardCharsets.UTF_8)); String source = entry.getValue().getSource(); if (source != null) digest.update(source.getBytes(java.nio.charset.StandardCharsets.UTF_8)); @@ -209,7 +226,7 @@ public final class ConfigurationCommand implements IDelegateCommandHandler { // Return project locations as well; the client excludes nested independent modules. roots.add(project.getProject().getLocationURI().toString()); builder.metadataDirectory(project.getProject().getLocation().toFile().toPath().resolve("src/main/resources"), project); - Index result = new Index(revision, workingCopies, List.copyOf(builder.entries), List.copyOf(roots), Map.copyOf(builder.files), builder.incomplete, builder.parsed); + Index result = new Index(revision, workingCopies, List.copyOf(builder.entries), List.copyOf(roots), Map.copyOf(builder.files), builder.incomplete, builder.parsed, GENERATION.incrementAndGet()); CACHE.remove(id); while (!CACHE.isEmpty() && (CACHE.size() >= 8 || CACHE.values().stream().mapToInt(value -> value.entries.size()).sum() + result.entries.size() > MAX_ENTRIES)) diff --git a/plugins/java-usages/tests/configuration.py b/plugins/java-usages/tests/configuration.py index 9a42f4a0..f218019f 100644 --- a/plugins/java-usages/tests/configuration.py +++ b/plugins/java-usages/tests/configuration.py @@ -200,6 +200,33 @@ try: warm = [] for _ in range(20): start = time.perf_counter(); query('app.time-out'); warm.append(time.perf_counter() - start) + # The snapshot stays warm across editor activity that cannot change it: + # opening/closing another file, a full build, and an edit once it is saved. + generation = query('app.time-out')['generation'] + other = source_dir / 'demo/NotSpring.java' + other_text = other.read_text() + send({'jsonrpc': '2.0', 'method': 'textDocument/didOpen', 'params': {'textDocument': {'uri': other.as_uri(), 'languageId': 'java', 'version': 1, 'text': other_text}}}) + assert query('app.time-out')['generation'] == generation, 'opening an editor rebuilt the index' + send({'jsonrpc': '2.0', 'method': 'textDocument/didClose', 'params': {'textDocument': {'uri': other.as_uri()}}}) + request('java/buildWorkspace', True) + assert query('app.time-out')['generation'] == generation, 'closing an editor or building rebuilt the index' + saved_text = other_text + '// saved\n' + send({'jsonrpc': '2.0', 'method': 'textDocument/didOpen', 'params': {'textDocument': {'uri': other.as_uri(), 'languageId': 'java', 'version': 1, 'text': other_text}}}) + send({'jsonrpc': '2.0', 'method': 'textDocument/didChange', 'params': {'textDocument': {'uri': other.as_uri(), 'version': 2}, 'contentChanges': [{'text': saved_text}]}}) + other.write_text(saved_text) + send({'jsonrpc': '2.0', 'method': 'textDocument/didSave', 'params': {'textDocument': {'uri': other.as_uri()}}}) + after_save = query('app.time-out')['generation'] + assert query('app.time-out')['generation'] == after_save, 'a saved editor keeps rebuilding the index' + send({'jsonrpc': '2.0', 'method': 'textDocument/didClose', 'params': {'textDocument': {'uri': other.as_uri()}}}) + after_close = query('app.time-out')['generation'] + assert query('app.time-out')['generation'] == after_close + # A source added on disk still invalidates it. + added = write('demo/Added.java', 'package demo; @org.springframework.boot.context.properties.ConfigurationProperties("added") class Added { int size; }') + send({'jsonrpc': '2.0', 'method': 'workspace/didChangeWatchedFiles', 'params': {'changes': [{'uri': added.as_uri(), 'type': 1}]}}) + deadline = time.monotonic() + 30 + while not query('added.size')['locations']: + assert time.monotonic() < deadline, 'a new source on disk did not reach the index' + time.sleep(0.2) # Unsaved changes must replace old keys, and closing must restore disk keys. changed = source.replace('prefix="app"', 'prefix="edited"').replace('${app.time-out:30}', '${edited.timeout:30}') send({'jsonrpc': '2.0', 'method': 'textDocument/didChange', 'params': {'textDocument': {'uri': file.as_uri(), 'version': 2}, 'contentChanges': [{'text': changed}]}}) @@ -211,7 +238,7 @@ try: send({'jsonrpc': '2.0', 'method': 'textDocument/didClose', 'params': {'textDocument': {'uri': file.as_uri()}}}) restored = query('app.nested.url') assert restored['locations'], restored - evidence = {'stress': os.environ.get('OPENIDEA_CONFIGURATION_STRESS') == '1', 'editRebuildMs': edit_ms, 'cases': results, 'warmMedianMs': sorted(warm)[len(warm)//2] * 1000, 'warmMaxMs': max(warm) * 1000} + evidence = {'stress': os.environ.get('OPENIDEA_CONFIGURATION_STRESS') == '1', 'editRebuildMs': edit_ms, 'cases': results, 'warmMedianMs': sorted(warm)[len(warm)//2] * 1000, 'warmMaxMs': max(warm) * 1000, 'generations': [generation, after_save, after_close]} (work / 'evidence.json').write_text(json.dumps(evidence, indent=2)) print(json.dumps(evidence, indent=2)) finally: