Skip to content

Commit

Permalink
Support offloading the node-model while a resource is loaded
Browse files Browse the repository at this point in the history
The node model consumes a lot of memory compared to the resource
contents. Languages may opt-in to offload the node model into a
serialized byte[] either in-memory or persisted somewhere else. It is
re-attached on demand.

There are some caveats, though. No strong references to original nodes
must be held. Otherwise the node model cannot be detached. If a code
path strictly needs to refer to a node,
DetachableParseResultWrapper.externalReference(IParseResult, INode) can
be used.

Feature can be enabled by using the DetachableNodeModelFragment. It is
subject to the language to chose a suitable moment in the life-cycle of
a resource to schedule unloading the node model.
DetableParseResultWrapper.acquire might be a candidate if all resources
shall be marked as unloadable immediately.

Also:
- Improved testability of node model
- New more flexible API for node model serialization/deserialization
- Allow to unbind a Guice binding from a generator fragment

Signed-off-by: Sebastian Zarnekow <sebastian.zarnekow@gmail.com>
  • Loading branch information
szarnekow committed Apr 15, 2024
1 parent f738a5b commit eb0420a
Show file tree
Hide file tree
Showing 78 changed files with 2,562 additions and 343 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,9 @@ public void clearResourceSet(ResourceSet resourceSet) {
boolean wasDeliver = resourceSet.eDeliver();
try {
resourceSet.eSetDeliver(false);
for (Resource resource : resourceSet.getResources()) {
resource.eSetDeliver(false);
}
resourceSet.getResources().clear();
} finally {
resourceSet.eSetDeliver(wasDeliver);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,29 @@
public class LoggingTester {
private static final Comparator<LogEntry> TEMPORAL_ORDER = ($0, $1) -> Longs.compare($0.timeStamp, $1.timeStamp);

/**
* @param level the log level to set on the given logger source
* @param source see {@link Logger#getLogger(Class)}
* @param action the task to run with captured logging
*/
public static LogCapture captureLogging(Level level, Class<?> source, Runnable action) {
Logger logger = Logger.getLogger(source);
return captureLogging(logger, level, action);
}

/**
* @since 2.35
*
* @param level the log level to set on the given logger source
* @param source see {@link Logger#getLogger(String)}
* @param action the task to run with captured logging
*/
public static LogCapture captureLogging(Level level, String source, Runnable action) {
Logger logger = Logger.getLogger(source);
return captureLogging(logger, level, action);
}

protected static LogCapture captureLogging(Logger logger, Level level, Runnable action) {
QueueAppender appender = new QueueAppender();
Level oldLevel = logger.getLevel();
List<Appender> allAppenders = appenderHierarchy(logger);
Expand Down
2 changes: 1 addition & 1 deletion org.eclipse.xtext.tests/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Bundle-Localization: plugin
Bundle-RequiredExecutionEnvironment: JavaSE-11
Require-Bundle: org.eclipse.xtext;bundle-version="2.35.0",
org.eclipse.emf.mwe.utils;bundle-version="1.10.0",
org.eclipse.emf.ecore;bundle-version="2.26.0",
org.eclipse.emf.ecore;bundle-version="2.26.0";visibility:=reexport,
org.eclipse.emf.common;bundle-version="2.24.0",
org.eclipse.emf.mwe2.launch;bundle-version="2.17.0";resolution:=optional;x-installation:=greedy,
org.eclipse.xtext.testlanguages;bundle-version="2.35.0",
Expand Down
2 changes: 1 addition & 1 deletion org.eclipse.xtext.tests/META-INF/MANIFEST.MF_gen
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ Require-Bundle: org.antlr.runtime;bundle-version="[3.2.0,3.2.1)",
org.eclipse.emf.ecore,
org.eclipse.xtext,
org.eclipse.xtext.testing,
org.eclipse.xtext.xbase.lib;bundle-version="2.34.0",
org.eclipse.xtext.xbase.lib;bundle-version="2.35.0",
org.eclipse.xtext.xbase.testing
Import-Package: org.apache.log4j
Automatic-Module-Name: org.eclipse.xtext.tests
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
import org.eclipse.xtext.enumrules.serializer.EnumAndReferenceTestLanguageSemanticSequencer;
import org.eclipse.xtext.enumrules.serializer.EnumAndReferenceTestLanguageSyntacticSequencer;
import org.eclipse.xtext.enumrules.services.EnumAndReferenceTestLanguageGrammarAccess;
import org.eclipse.xtext.nodemodel.detachable.DetachableNodeModelBuilder;
import org.eclipse.xtext.nodemodel.detachable.DetachableParseResultWrapper;
import org.eclipse.xtext.nodemodel.impl.NodeModelBuilder;
import org.eclipse.xtext.parser.IParser;
import org.eclipse.xtext.parser.ITokenToStringConverter;
import org.eclipse.xtext.parser.antlr.AntlrTokenDefProvider;
Expand All @@ -29,6 +32,8 @@
import org.eclipse.xtext.parser.antlr.Lexer;
import org.eclipse.xtext.parser.antlr.LexerBindings;
import org.eclipse.xtext.parser.antlr.LexerProvider;
import org.eclipse.xtext.parser.impl.PartialParsingHelper;
import org.eclipse.xtext.resource.ParseResultWrapper;
import org.eclipse.xtext.serializer.ISerializer;
import org.eclipse.xtext.serializer.impl.Serializer;
import org.eclipse.xtext.serializer.sequencer.ISemanticSequencer;
Expand Down Expand Up @@ -120,4 +125,19 @@ public Class<? extends ISerializer> bindISerializer() {
return Serializer.class;
}

// contributed by org.eclipse.xtext.xtext.generator.parser.DetachableNodeModelFragment
public Class<? extends NodeModelBuilder> bindNodeModelBuilder() {
return DetachableNodeModelBuilder.class;
}

// contributed by org.eclipse.xtext.xtext.generator.parser.DetachableNodeModelFragment
public Class<? extends ParseResultWrapper> bindParseResultWrapper() {
return DetachableParseResultWrapper.class;
}

// contributed by org.eclipse.xtext.xtext.generator.parser.DetachableNodeModelFragment
public Class<? extends PartialParsingHelper> bindPartialParsingHelper() {
return null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
import org.eclipse.xtext.enumrules.serializer.EnumRulesTestLanguageSemanticSequencer;
import org.eclipse.xtext.enumrules.serializer.EnumRulesTestLanguageSyntacticSequencer;
import org.eclipse.xtext.enumrules.services.EnumRulesTestLanguageGrammarAccess;
import org.eclipse.xtext.nodemodel.detachable.DetachableNodeModelBuilder;
import org.eclipse.xtext.nodemodel.detachable.DetachableParseResultWrapper;
import org.eclipse.xtext.nodemodel.impl.NodeModelBuilder;
import org.eclipse.xtext.parser.IParser;
import org.eclipse.xtext.parser.ITokenToStringConverter;
import org.eclipse.xtext.parser.antlr.AntlrTokenDefProvider;
Expand All @@ -29,6 +32,8 @@
import org.eclipse.xtext.parser.antlr.Lexer;
import org.eclipse.xtext.parser.antlr.LexerBindings;
import org.eclipse.xtext.parser.antlr.LexerProvider;
import org.eclipse.xtext.parser.impl.PartialParsingHelper;
import org.eclipse.xtext.resource.ParseResultWrapper;
import org.eclipse.xtext.serializer.ISerializer;
import org.eclipse.xtext.serializer.impl.Serializer;
import org.eclipse.xtext.serializer.sequencer.ISemanticSequencer;
Expand Down Expand Up @@ -120,4 +125,19 @@ public Class<? extends ISerializer> bindISerializer() {
return Serializer.class;
}

// contributed by org.eclipse.xtext.xtext.generator.parser.DetachableNodeModelFragment
public Class<? extends NodeModelBuilder> bindNodeModelBuilder() {
return DetachableNodeModelBuilder.class;
}

// contributed by org.eclipse.xtext.xtext.generator.parser.DetachableNodeModelFragment
public Class<? extends ParseResultWrapper> bindParseResultWrapper() {
return DetachableParseResultWrapper.class;
}

// contributed by org.eclipse.xtext.xtext.generator.parser.DetachableNodeModelFragment
public Class<? extends PartialParsingHelper> bindPartialParsingHelper() {
return null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
import org.eclipse.xtext.enumrules.serializer.MultiRuleEnumTestLanguageSemanticSequencer;
import org.eclipse.xtext.enumrules.serializer.MultiRuleEnumTestLanguageSyntacticSequencer;
import org.eclipse.xtext.enumrules.services.MultiRuleEnumTestLanguageGrammarAccess;
import org.eclipse.xtext.nodemodel.detachable.DetachableNodeModelBuilder;
import org.eclipse.xtext.nodemodel.detachable.DetachableParseResultWrapper;
import org.eclipse.xtext.nodemodel.impl.NodeModelBuilder;
import org.eclipse.xtext.parser.IParser;
import org.eclipse.xtext.parser.ITokenToStringConverter;
import org.eclipse.xtext.parser.antlr.AntlrTokenDefProvider;
Expand All @@ -29,6 +32,8 @@
import org.eclipse.xtext.parser.antlr.Lexer;
import org.eclipse.xtext.parser.antlr.LexerBindings;
import org.eclipse.xtext.parser.antlr.LexerProvider;
import org.eclipse.xtext.parser.impl.PartialParsingHelper;
import org.eclipse.xtext.resource.ParseResultWrapper;
import org.eclipse.xtext.serializer.ISerializer;
import org.eclipse.xtext.serializer.impl.Serializer;
import org.eclipse.xtext.serializer.sequencer.ISemanticSequencer;
Expand Down Expand Up @@ -120,4 +125,19 @@ public Class<? extends ISerializer> bindISerializer() {
return Serializer.class;
}

// contributed by org.eclipse.xtext.xtext.generator.parser.DetachableNodeModelFragment
public Class<? extends NodeModelBuilder> bindNodeModelBuilder() {
return DetachableNodeModelBuilder.class;
}

// contributed by org.eclipse.xtext.xtext.generator.parser.DetachableNodeModelFragment
public Class<? extends ParseResultWrapper> bindParseResultWrapper() {
return DetachableParseResultWrapper.class;
}

// contributed by org.eclipse.xtext.xtext.generator.parser.DetachableNodeModelFragment
public Class<? extends PartialParsingHelper> bindPartialParsingHelper() {
return null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -193,14 +193,17 @@ Workflow {
language = {
grammarUri = "classpath:/org/eclipse/xtext/enumrules/EnumRulesTestLanguage.xtext"
fragment = @TestLanguagesFragments {}
fragment = nodemodel.detachable.DetachableNodeModelFragment {}
}
language = {
grammarUri = "classpath:/org/eclipse/xtext/enumrules/EnumAndReferenceTestLanguage.xtext"
fragment = @TestLanguagesFragments {}
fragment = nodemodel.detachable.DetachableNodeModelFragment {}
}
language = {
grammarUri = "classpath:/org/eclipse/xtext/enumrules/MultiRuleEnumTestLanguage.xtext"
fragment = @TestLanguagesFragments {}
fragment = nodemodel.detachable.DetachableNodeModelFragment {}
}

language = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*******************************************************************************
* Copyright (c) 2024 Sebastian Zarnekow and others.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.eclipse.xtext.enumrules;

import java.io.IOException;
import java.time.Duration;

import org.apache.log4j.Level;
import org.eclipse.xtext.enumrules.parser.antlr.EnumAndReferenceTestLanguageParser;
import org.eclipse.xtext.nodemodel.ICompositeNode;
import org.eclipse.xtext.nodemodel.detachable.DetachableNodeModelBuilder;
import org.eclipse.xtext.nodemodel.detachable.DetachableParseResult;
import org.eclipse.xtext.nodemodel.detachable.GrammarElementLookup;
import org.eclipse.xtext.nodemodel.impl.NodeModelBuilder;
import org.eclipse.xtext.parser.IParser;
import org.eclipse.xtext.parser.ParserTestHelper;
import org.eclipse.xtext.resource.XtextResource;
import org.eclipse.xtext.testing.logging.LoggingTester;
import org.eclipse.xtext.testing.logging.LoggingTester.LogCapture;
import org.junit.Test;

import com.google.inject.Inject;

/**
* @author Sebastian - Initial contribution and API
*/
public class AutoDetachNodeModelTest extends AbstractEnumRulesTest {

private ParserTestHelper helper;

@Override
public void setUp() throws Exception {
super.setUp();
with(EnumAndReferenceTestLanguageStandaloneSetup.class);
helper = new ParserTestHelper(getResourceFactory(), getParser(), get(Keys.RESOURCE_SET_KEY),getCurrentFileExtension());
}

static class CustomParser extends EnumAndReferenceTestLanguageParser {

@Inject GrammarElementLookup grammarElementLookup;

@Override
protected NodeModelBuilder createNodeModelBuilder() {
return new DetachableNodeModelBuilder(grammarElementLookup) {
@Override
protected DetachableParseResult createEmptyParseResult(GrammarElementLookup grammarElementLookup) {
return new DetachableParseResult(grammarElementLookup, Duration.ofMillis(50));
}
};
}
}

@Override
protected IParser getParser() {
return getInjector().getInstance(CustomParser.class);
}

@Test public void testNodeModelIsAutoReleased() throws Exception {
LogCapture messages = LoggingTester.captureLogging(Level.TRACE, "org.eclipse.xtext.nodemodel.detachable", ()->{
try {
String modelAsString = "kindOfKeyword Hoo reference Hoo";
XtextResource resource = parse(modelAsString);
DetachableParseResult parseResult = (DetachableParseResult) resource.getParseResult();
ICompositeNode rootNode = parseResult.getRootNode();
parseResult.releaseNodeModel(false);

Thread.sleep(200);
assertNotSame(rootNode, parseResult.getRootNode());
} catch(Exception e) {
throw new RuntimeException(e);
}
});
messages.assertNumberOfLogEntries(2, "scheduleRelease");
messages.assertLogEntry("serialize:NodeModelData");
messages.assertLogEntry("nodeModelRequested");
messages.assertLogEntry("deserialize:StandardNodeModelReference");
}

@Test public void testNodeModelIsAutoReleasedAfterUsage() throws Exception {
LogCapture messages = LoggingTester.captureLogging(Level.TRACE, "org.eclipse.xtext.nodemodel.detachable", ()->{
try {
String modelAsString = "kindOfKeyword Hoo reference Hoo";
XtextResource resource = parse(modelAsString);
DetachableParseResult parseResult = (DetachableParseResult) resource.getParseResult();
ICompositeNode rootNode = parseResult.getRootNode();
parseResult.releaseNodeModel(false);

Thread.sleep(200);
assertNotSame(rootNode, parseResult.getRootNode());
Thread.sleep(200);
} catch(Exception e) {
throw new RuntimeException(e);
}
});
messages.assertNumberOfLogEntries(2, "scheduleRelease");
messages.assertLogEntry("serialize:NodeModelData");
messages.assertLogEntry("nodeModelRequested");
messages.assertLogEntry("deserialize:StandardNodeModelReference");
messages.assertLogEntry("release");
}

protected XtextResource parse(String modelAsString) throws IOException {
return helper.getResourceFromString(modelAsString);
}
}
Loading

0 comments on commit eb0420a

Please sign in to comment.