Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Core] Improve caching glue performance #2971

Merged
merged 13 commits into from
Feb 25, 2025
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Changed
- [Core] Improved caching glue performance ([#2971](https://github.com/cucumber/cucumber-jvm/pull/2971) M.P. Korstanje & Julien Kronegg)
- [Java, Java8] Significantly reduced number of emitted step- and hook-definition messages ([#2971](https://github.com/cucumber/cucumber-jvm/pull/2971) M.P. Korstanje & Julien Kronegg)
- [Core] Removed workarounds to limit size of html report ([#2971](https://github.com/cucumber/cucumber-jvm/pull/2971) M.P. Korstanje & Julien Kronegg)

### Deprecated
- [Core] Deprecated `ScenarioScoped` glue ([#2971](https://github.com/cucumber/cucumber-jvm/pull/2971) M.P. Korstanje & Julien Kronegg)

## [7.21.1] - 2025-02-07
### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
* Instances of scenario scoped glue can not be used between scenarios and will
* be removed from the glue. This is useful when the glue holds a reference to a
* scenario scoped object (e.g. a method closure).
*
* @deprecated backend with scenario scoped glue should hide this complexity
* from Cucumber by updating the registered glue during
* {@link Backend#buildWorld()} and transparently dispose of any
* closures during {@link Backend#disposeWorld()}.
*/
@Deprecated
public interface ScenarioScoped {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,6 @@ public void setEventPublisher(EventPublisher publisher) {
}

private void write(Envelope event) {
// Workaround to reduce the size of the report
// See: https://github.com/cucumber/cucumber/issues/1232
if (event.getStepDefinition().isPresent() || event.getHook().isPresent()
|| event.getParameterType().isPresent()) {
return;
}

try {
writer.write(event);
} catch (IOException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;

Expand Down Expand Up @@ -75,14 +76,20 @@ final class CachingGlue implements Glue {
/*
* Storing the pattern that matches the step text allows us to cache the
* rather slow regex comparisons in `stepDefinitionMatches`. This cache does
* not need to be cleaned. The matching pattern be will used to look up a
* not need to be cleaned. The matching pattern be will be used to look up a
* pickle specific step definition from `stepDefinitionsByPattern`.
*/
private final Map<String, String> stepPatternByStepText = new HashMap<>();
private final Map<String, CoreStepDefinition> stepDefinitionsByPattern = new TreeMap<>();

private final EventBus bus;

private StepTypeRegistry stepTypeRegistry;
private Locale locale = null;
private StepExpressionFactory stepExpressionFactory = null;
private boolean cacheIsDirty = false;
private boolean hasScenarioScopedGlue = false;

CachingGlue(EventBus bus) {
this.bus = bus;
}
Expand All @@ -102,35 +109,43 @@ public void addAfterAllHook(StaticHookDefinition afterAllHook) {
@Override
public void addStepDefinition(StepDefinition stepDefinition) {
stepDefinitions.add(stepDefinition);
cacheIsDirty = true;
hasScenarioScopedGlue |= stepDefinition instanceof ScenarioScoped;
}

@Override
public void addBeforeHook(HookDefinition hookDefinition) {
beforeHooks.add(CoreHookDefinition.create(hookDefinition, bus::generateId));
beforeHooks.sort(HOOK_ORDER_ASCENDING);
hasScenarioScopedGlue |= hookDefinition instanceof ScenarioScoped;
}

@Override
public void addAfterHook(HookDefinition hookDefinition) {
afterHooks.add(CoreHookDefinition.create(hookDefinition, bus::generateId));
afterHooks.sort(HOOK_ORDER_ASCENDING);
hasScenarioScopedGlue |= hookDefinition instanceof ScenarioScoped;
}

@Override
public void addBeforeStepHook(HookDefinition hookDefinition) {
beforeStepHooks.add(CoreHookDefinition.create(hookDefinition, bus::generateId));
beforeStepHooks.sort(HOOK_ORDER_ASCENDING);
hasScenarioScopedGlue |= hookDefinition instanceof ScenarioScoped;
}

@Override
public void addAfterStepHook(HookDefinition hookDefinition) {
afterStepHooks.add(CoreHookDefinition.create(hookDefinition, bus::generateId));
afterStepHooks.sort(HOOK_ORDER_ASCENDING);
hasScenarioScopedGlue |= hookDefinition instanceof ScenarioScoped;
}

@Override
public void addParameterType(ParameterTypeDefinition parameterType) {
parameterTypeDefinitions.add(parameterType);
cacheIsDirty = true;
hasScenarioScopedGlue |= parameterType instanceof ScenarioScoped;
}

@Override
Expand Down Expand Up @@ -229,10 +244,29 @@ List<DocStringTypeDefinition> getDocStringTypeDefinitions() {
return docStringTypeDefinitions;
}

void prepareGlue(StepTypeRegistry stepTypeRegistry) throws DuplicateStepDefinitionException {
StepExpressionFactory stepExpressionFactory = new StepExpressionFactory(stepTypeRegistry, bus);
StepTypeRegistry getStepTypeRegistry() {
return stepTypeRegistry;
}

void prepareGlue(Locale locale) throws DuplicateStepDefinitionException {
boolean firstTime = stepTypeRegistry == null;
boolean languageChanged = !locale.equals(this.locale);
if (!firstTime && !languageChanged && !cacheIsDirty && !hasScenarioScopedGlue) {
return;
}
// conditions changed => invalidate the glue cache
// Note: we have a prudent approach of avoiding caching if
// scenario-scoped glue exist (e.g. cucumber-java8).
this.locale = locale;
stepTypeRegistry = new StepTypeRegistry(locale);
stepExpressionFactory = new StepExpressionFactory(stepTypeRegistry, bus);
stepDefinitionsByPattern.clear();
stepPatternByStepText.clear();
// since we must rebuild the cache, it will not be dirty the next time
cacheIsDirty = false;

// TODO: separate prepared and unprepared glue into different classes
// parameters changed from the previous scenario => re-register them
parameterTypeDefinitions.forEach(ptd -> {
ParameterType<?> parameterType = ptd.parameterType();
stepTypeRegistry.defineParameterType(parameterType);
Expand Down Expand Up @@ -442,7 +476,9 @@ private List<PickleStepDefinitionMatch> stepDefinitionMatches(URI uri, Step step
}

void removeScenarioScopedGlue() {
stepDefinitionsByPattern.clear();
if (!hasScenarioScopedGlue) {
return;
}
removeScenarioScopedGlue(beforeHooks);
removeScenarioScopedGlue(beforeStepHooks);
removeScenarioScopedGlue(afterHooks);
Expand All @@ -454,6 +490,7 @@ void removeScenarioScopedGlue() {
removeScenarioScopedGlue(defaultParameterTransformers);
removeScenarioScopedGlue(defaultDataTableEntryTransformers);
removeScenarioScopedGlue(defaultDataTableCellTransformers);
hasScenarioScopedGlue = false;
}

private void removeScenarioScopedGlue(Iterable<?> glues) {
Expand All @@ -464,6 +501,7 @@ private void removeScenarioScopedGlue(Iterable<?> glues) {
ScenarioScoped scenarioScoped = (ScenarioScoped) glue;
scenarioScoped.dispose();
glueIterator.remove();
cacheIsDirty = true;
}
}
}
Expand Down
13 changes: 7 additions & 6 deletions cucumber-core/src/main/java/io/cucumber/core/runner/Runner.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

Expand All @@ -39,6 +41,7 @@ public final class Runner {
private final Collection<? extends Backend> backends;
private final Options runnerOptions;
private final ObjectFactory objectFactory;
private final Map<String, Locale> localeCache = new HashMap<>();
private List<SnippetGenerator> snippetGenerators;

public Runner(
Expand All @@ -63,13 +66,12 @@ public EventBus getBus() {

public void runPickle(Pickle pickle) {
try {
StepTypeRegistry stepTypeRegistry = createTypeRegistryForPickle(pickle);
snippetGenerators = createSnippetGeneratorsForPickle(stepTypeRegistry);

// Java8 step definitions will be added to the glue here
buildBackendWorlds();

glue.prepareGlue(stepTypeRegistry);
glue.prepareGlue(localeForPickle(pickle));
snippetGenerators = createSnippetGeneratorsForPickle(glue.getStepTypeRegistry());

TestCase testCase = createTestCaseForPickle(pickle);
testCase.run(bus);
Expand All @@ -79,10 +81,9 @@ public void runPickle(Pickle pickle) {
}
}

private StepTypeRegistry createTypeRegistryForPickle(Pickle pickle) {
private Locale localeForPickle(Pickle pickle) {
String language = pickle.getLanguage();
Locale locale = new Locale(language);
return new StepTypeRegistry(locale);
return localeCache.computeIfAbsent(language, (lang) -> new Locale(language));
}

public void runBeforeAllHooks() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,13 @@
import io.cucumber.core.eventbus.EventBus;
import io.cucumber.core.runtime.TimeServiceEventBus;
import io.cucumber.messages.types.Envelope;
import io.cucumber.messages.types.Hook;
import io.cucumber.messages.types.ParameterType;
import io.cucumber.messages.types.SourceReference;
import io.cucumber.messages.types.StepDefinition;
import io.cucumber.messages.types.StepDefinitionPattern;
import io.cucumber.messages.types.StepDefinitionPatternType;
import io.cucumber.messages.types.TestRunFinished;
import io.cucumber.messages.types.TestRunStarted;
import io.cucumber.messages.types.Timestamp;
import org.junit.jupiter.api.Test;

import java.io.ByteArrayOutputStream;
import java.time.Clock;
import java.util.Collections;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -48,54 +41,6 @@ void writes_index_html() throws Throwable {
extractCucumberMessages(bytes), STRICT);
}

@Test
void ignores_step_definitions() throws Throwable {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
HtmlFormatter formatter = new HtmlFormatter(bytes);
EventBus bus = new TimeServiceEventBus(Clock.systemUTC(), UUID::randomUUID);
formatter.setEventPublisher(bus);

TestRunStarted testRunStarted = new TestRunStarted(new Timestamp(10L, 0L), null);
bus.send(Envelope.of(testRunStarted));

StepDefinition stepDefinition = new StepDefinition(
"",
new StepDefinitionPattern("", StepDefinitionPatternType.CUCUMBER_EXPRESSION),
SourceReference.of("https://example.com"));
bus.send(Envelope.of(stepDefinition));

Hook hook = new Hook("",
null,
SourceReference.of("https://example.com"),
null, null);
bus.send(Envelope.of(hook));

// public ParameterType(String name, List<String> regularExpressions,
// Boolean preferForRegularExpressionMatch, Boolean useForSnippets,
// String id) {
ParameterType parameterType = new ParameterType(
"",
Collections.emptyList(),
true,
false,
"",
null);
bus.send(Envelope.of(parameterType));

TestRunFinished testRunFinished = new TestRunFinished(
null,
true,
new Timestamp(15L, 0L),
null, null);
bus.send(Envelope.of(testRunFinished));

assertEquals("[" +
"{\"testRunStarted\":{\"timestamp\":{\"nanos\":0,\"seconds\":10}}}," +
"{\"testRunFinished\":{\"success\":true,\"timestamp\":{\"nanos\":0,\"seconds\":15}}}" +
"]",
extractCucumberMessages(bytes), STRICT);
}

private static String extractCucumberMessages(ByteArrayOutputStream bytes) {
Pattern pattern = Pattern.compile("^.*window\\.CUCUMBER_MESSAGES = (\\[.+]);.*$", Pattern.DOTALL);
Matcher matcher = pattern.matcher(new String(bytes.toByteArray(), UTF_8));
Expand Down
Loading
Loading