Skip to content

Commit

Permalink
[Core] Add faster UUID generator selectable through SPI (#2703)
Browse files Browse the repository at this point in the history
The cucumber event bus UUID generator can now be configured through a SPI
using a property from:
- command-line
- environment variables
- system properties
- `cucumber.properties`
- `junit-platform.properties`
- `@CucumberOptions`

Two UUID generators are provided:

An event has a UUID. The UUID generator can be configured using the
`cucumber.uuid-generator` property:

- name: io.cucumber.core.eventbus.RandomUuidGenerator*
  features: Thread-safe, collision-free, multi-jvm
  performance (Millions UUID/second):  ~1
  Typical usage example: Reports may be generated on different JVMs at
     the same time. A typical example would be one suite that tests
     against Firefox and another against Safari. The exact browser is
     configured through a property. These are then executed concurrently
     on different Gitlab runners.
- name: io.cucumber.core.eventbus.IncrementingUuidGenerator  
  features: Thread-safe, collision-free, single-jvm
  performance (Millions UUID/second): ~130
  Typical usage example: Reports are generated on a single JVM                                                                                                                                                                                                                                          |

Performance on real projects depends on the size (e.g. on a project with
3095 Given, 445 When, 1177 Then, 445 Scenarios, 48 Rules, the `IncrementingUuidGenerator`
improves the global performance by about 3-5% and the feature file 
parsing performance of about 37%).

The `IncrementingUuidGenerator` is a very simple UUID generator based 
on `AtomicLong` counters. It is thread-safe and collision-free within
a single JVM.

The `RandomUuidGenerator` is the usual `UUID.randomUUID()` generator.

Fixes: #2698
  • Loading branch information
jkronegg authored Apr 6, 2023
1 parent 5061742 commit 5691d30
Show file tree
Hide file tree
Showing 43 changed files with 1,468 additions and 50 deletions.
24 changes: 24 additions & 0 deletions .revapi/api-changes.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,18 @@
"code": "java.method.finalMethodAddedToNonFinalClass",
"new": "method java.lang.Long io.cucumber.core.internal.com.fasterxml.jackson.databind.deser.std.StdDeserializer<T>::_parseLong(io.cucumber.core.internal.com.fasterxml.jackson.databind.DeserializationContext, java.lang.String) throws java.io.IOException",
"justification": "Internal API"
},
{
"ignore": true,
"code": "java.method.addedToInterface",
"new": "method java.lang.Class<? extends io.cucumber.core.eventbus.UuidGenerator> io.cucumber.core.options.CucumberOptionsAnnotationParser.CucumberOptions::uuidGenerator()",
"justification": "Internal API"
},
{
"ignore": true,
"code": "java.method.addedToInterface",
"new": "method java.lang.Class<? extends io.cucumber.core.eventbus.UuidGenerator> io.cucumber.core.runner.Options::getUuidGeneratorClass()",
"justification": "Internal API"
}
]
}
Expand Down Expand Up @@ -331,6 +343,12 @@
"code": "java.method.defaultMethodAddedToInterface",
"new": "method java.util.Set<org.testng.ITestNGMethod> org.testng.ITestNGMethod::upstreamDependencies()",
"justification": "Third party api change"
},
{
"ignore": true,
"code": "java.class.externalClassExposedInAPI",
"new": "interface io.cucumber.core.eventbus.UuidGenerator",
"justification": "Part of cucumber API"
}
]
}
Expand Down Expand Up @@ -383,6 +401,12 @@
"new": "method int org.junit.platform.engine.ConfigurationParameters::size()",
"annotation": "@org.apiguardian.api.API(status = org.apiguardian.api.API.Status.DEPRECATED, since = \"1.9\")",
"justification": "API consumed from JUnit 5"
},
{
"ignore": true,
"code": "java.class.externalClassExposedInAPI",
"new": "interface io.cucumber.core.eventbus.UuidGenerator",
"justification": "Part of cucumber API"
}
]
}
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- [JUnit Platform Engine] Add constant for fixed.max-pool-size property ([#2713](https://github.com/cucumber/cucumber-jvm/pull/2713) M.P. Korstanje)

### Added
- [Core] Improved event bus performance using UUID generator selectable through SPI ([#2703](https://github.com/cucumber/cucumber-jvm/pull/2703) Julien Kronegg)

## [7.11.2] - 2023-03-23
### Fixed
- [JUnit Platform Engine] Corrupted junit-xml report when using `surefire.rerunFailingTestsCount` parameter ([#2709](https://github.com/cucumber/cucumber-jvm/pull/2709) M.P. Korstanje)
Expand Down
20 changes: 20 additions & 0 deletions cucumber-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ cucumber.plugin= # comma separated plugin strings.
cucumber.object-factory= # object factory class name.
# example: com.example.MyObjectFactory
cucumber.uuid-generator= # UUID generator class name.
# example: com.example.MyUuidGenerator
cucumber.publish.enabled # true or false. default: false
# enable publishing of test results
Expand Down Expand Up @@ -79,6 +82,23 @@ They are respectively responsible for discovering glue classes, registering
step definitions, and creating instances of said glue classes. Backend and
object factory implementations are discovered via SPI.

## Event bus ##

Cucumber emits events on an event bus in many cases:
- during the feature file parsing
- when the test scenarios are executed

An event has a UUID. The UUID generator can be configured using the `cucumber.uuid-generator` property:

| UUID generator | Features | Performance [Millions UUID/second] | Typical usage example |
|-----------------------------------------------------|-----------------------------------------|------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| io.cucumber.core.eventbus.RandomUuidGenerator | Thread-safe, collision-free, multi-jvm | ~1 | Reports may be generated on different JVMs at the same time. A typical example would be one suite that tests against Firefox and another against Safari. The exact browser is configured through a property. These are then executed concurrently on different Gitlab runners. |
| io.cucumber.core.eventbus.IncrementingUuidGenerator | Thread-safe, collision-free, single-jvm | ~130 | Reports are generated on a single JVM |

The performance gain on real project depend on the feature size.

When not specified, the `RandomUuidGenerator` is used.

## Plugin ##

By implementing the Plugin interface classes can listen to execution events
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ public final class CommandlineOptions {

public static final String OBJECT_FACTORY = "--object-factory";

public static final String UUID_GENERATOR = "--uuid-generator";

private CommandlineOptions() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package io.cucumber.core.eventbus;

import io.cucumber.core.exception.CucumberException;

import java.util.Random;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;

/**
* Thread-safe and collision-free UUID generator for single JVM. This is a
* sequence generator and each instance has its own counter. This generator is
* about 100 times faster than #RandomUuidGenerator.
*
* Properties:
* - thread-safe
* - collision-free in the same classloader
* - almost collision-free in different classloaders / JVMs
* - UUIDs generated using the instances from the same classloader are sortable
*
* UUID version 8 (custom) / variant 2 <a href=
* "https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-8">...</a>
* <!-- @formatter:off -->
* | 40 bits | 8 bits | 4 bits | 12 bits | 2 bits | 62 bits |
* | -------------------| -------------- | ------- | ------------- | ------- | ------- |
* | LSBs of epoch-time | sessionCounter | version | classloaderId | variant | counter |
* <!-- @formatter:on -->
*/
public class IncrementingUuidGenerator implements UuidGenerator {
/**
* 40 bits mask for the epoch-time part (MSB).
*/
private static final long MAX_EPOCH_TIME = 0x0ffffffffffL;

/**
* 8 bits mask for the session identifier (MSB). Package-private for testing
* purposes.
*/
static final long MAX_SESSION_ID = 0xffL;

/**
* 62 bits mask for the counter value (LSB)
*/
static final long MAX_COUNTER_VALUE = 0x3fffffffffffffffL;

/**
* Classloader identifier (MSB). The identifier is a pseudo-random number on
* 12 bits. Note: there is no need to save the Random because it's static.
*/
@SuppressWarnings("java:S2119")
static final long CLASSLOADER_ID = new Random().nextInt() & 0x0fff;

/**
* Session counter to differentiate instances created within a given
* classloader (MSB).
*/
static final AtomicLong sessionCounter = new AtomicLong(-1);

/**
* Computed UUID MSB value.
*/
final long msb;

/**
* Counter for the UUID LSB.
*/
final AtomicLong counter = new AtomicLong(-1);

public IncrementingUuidGenerator() {
long sessionId = sessionCounter.incrementAndGet();
if (sessionId == MAX_SESSION_ID) {
throw new CucumberException(
"Out of " + IncrementingUuidGenerator.class.getSimpleName() +
" capacity. Please reuse existing instances or use another " +
UuidGenerator.class.getSimpleName() + " implementation instead.");
}
long epochTime = System.currentTimeMillis();
// msb = epochTime | sessionId | version | classloaderId
msb = ((epochTime & MAX_EPOCH_TIME) << 24) | (sessionId << 16) | (8 << 12) | CLASSLOADER_ID;
}

/**
* Generate a new UUID. Will throw an exception when out of capacity.
*
* @return a non-null UUID
* @throws CucumberException when out of capacity
*/
@Override
public UUID generateId() {
long counterValue = counter.incrementAndGet();
if (counterValue == MAX_COUNTER_VALUE) {
throw new CucumberException(
"Out of " + IncrementingUuidGenerator.class.getSimpleName() +
" capacity. Please generate using a new instance or use another " +
UuidGenerator.class.getSimpleName() + "implementation.");
}
long leastSigBits = counterValue | 0x8000000000000000L; // set variant
return new UUID(msb, leastSigBits);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.cucumber.core.eventbus;

public interface Options {

Class<? extends UuidGenerator> getUuidGeneratorClass();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.cucumber.core.eventbus;

import java.util.UUID;

/**
* UUID generator based on random numbers. The generator is thread-safe and
* supports multi-jvm usage of Cucumber.
*/
public class RandomUuidGenerator implements UuidGenerator {
@Override
public UUID generateId() {
return UUID.randomUUID();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.cucumber.core.eventbus;

import org.apiguardian.api.API;

import java.util.UUID;
import java.util.function.Supplier;

/**
* SPI (Service Provider Interface) to generate UUIDs.
*/
@API(status = API.Status.EXPERIMENTAL)
public interface UuidGenerator extends Supplier<UUID> {
UUID generateId();

default UUID get() {
return generateId();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,14 @@
import static io.cucumber.core.cli.CommandlineOptions.TAGS;
import static io.cucumber.core.cli.CommandlineOptions.TAGS_SHORT;
import static io.cucumber.core.cli.CommandlineOptions.THREADS;
import static io.cucumber.core.cli.CommandlineOptions.UUID_GENERATOR;
import static io.cucumber.core.cli.CommandlineOptions.VERSION;
import static io.cucumber.core.cli.CommandlineOptions.VERSION_SHORT;
import static io.cucumber.core.cli.CommandlineOptions.WIP;
import static io.cucumber.core.cli.CommandlineOptions.WIP_SHORT;
import static io.cucumber.core.options.ObjectFactoryParser.parseObjectFactory;
import static io.cucumber.core.options.OptionsFileParser.parseFeatureWithLinesFile;
import static io.cucumber.core.options.UuidGeneratorParser.parseUuidGenerator;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.joining;
Expand Down Expand Up @@ -167,6 +169,9 @@ private RuntimeOptionsBuilder parse(List<String> args) {
} else if (arg.equals(OBJECT_FACTORY)) {
String objectFactoryClassName = removeArgFor(arg, args);
parsedOptions.setObjectFactoryClass(parseObjectFactory(objectFactoryClassName));
} else if (arg.equals(UUID_GENERATOR)) {
String uuidGeneratorClassName = removeArgFor(arg, args);
parsedOptions.setUuidGeneratorClass(parseUuidGenerator(uuidGeneratorClassName));
} else if (arg.startsWith("-")) {
out.println("Unknown option: " + arg);
printUsage();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.cucumber.core.options;

import io.cucumber.core.runtime.ObjectFactoryServiceLoader;
import io.cucumber.core.runtime.UuidGeneratorServiceLoader;

public final class Constants {

Expand Down Expand Up @@ -118,6 +119,14 @@ public final class Constants {
*/
public static final String OBJECT_FACTORY_PROPERTY_NAME = "cucumber.object-factory";

/**
* Property name used to select a specific UUID generator implementation:
* {@value}
*
* @see UuidGeneratorServiceLoader
*/
public static final String UUID_GENERATOR_PROPERTY_NAME = "cucumber.uuid-generator";

/**
* Property name formerly used to pass command line options to Cucumber:
* {@value} This property is no longer read by Cucumber. Please use any of
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.cucumber.core.options;

import io.cucumber.core.backend.ObjectFactory;
import io.cucumber.core.eventbus.UuidGenerator;
import io.cucumber.core.exception.CucumberException;
import io.cucumber.core.feature.FeatureWithLines;
import io.cucumber.core.feature.GluePath;
Expand Down Expand Up @@ -45,6 +46,7 @@ public RuntimeOptionsBuilder parse(Class<?> clazz) {
addGlue(options, args);
addFeatures(options, args);
addObjectFactory(options, args);
addUuidGenerator(options, args);
}
}

Expand Down Expand Up @@ -149,6 +151,12 @@ private void addObjectFactory(CucumberOptions options, RuntimeOptionsBuilder arg
}
}

private void addUuidGenerator(CucumberOptions options, RuntimeOptionsBuilder args) {
if (options.uuidGenerator() != null) {
args.setUuidGeneratorClass(options.uuidGenerator());
}
}

private void addDefaultFeaturePathIfNoFeaturePathIsSpecified(RuntimeOptionsBuilder args, Class<?> clazz) {
if (!featuresSpecified) {
String packageName = packagePath(clazz);
Expand Down Expand Up @@ -208,6 +216,8 @@ public interface CucumberOptions {

Class<? extends ObjectFactory> objectFactory();

Class<? extends UuidGenerator> uuidGenerator();

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import static io.cucumber.core.options.Constants.PLUGIN_PUBLISH_QUIET_PROPERTY_NAME;
import static io.cucumber.core.options.Constants.PLUGIN_PUBLISH_TOKEN_PROPERTY_NAME;
import static io.cucumber.core.options.Constants.SNIPPET_TYPE_PROPERTY_NAME;
import static io.cucumber.core.options.Constants.UUID_GENERATOR_PROPERTY_NAME;
import static io.cucumber.core.options.Constants.WIP_PROPERTY_NAME;
import static io.cucumber.core.options.OptionsFileParser.parseFeatureWithLinesFile;
import static java.util.Arrays.stream;
Expand Down Expand Up @@ -102,6 +103,11 @@ public RuntimeOptionsBuilder parse(CucumberPropertiesProvider properties) {
ObjectFactoryParser::parseObjectFactory,
builder::setObjectFactoryClass);

parse(properties,
UUID_GENERATOR_PROPERTY_NAME,
UuidGeneratorParser::parseUuidGenerator,
builder::setUuidGeneratorClass);

parse(properties,
OPTIONS_PROPERTY_NAME,
identity(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.cucumber.core.options;

import io.cucumber.core.backend.ObjectFactory;
import io.cucumber.core.eventbus.UuidGenerator;
import io.cucumber.core.feature.FeatureWithLines;
import io.cucumber.core.order.PickleOrder;
import io.cucumber.core.order.StandardPickleOrders;
Expand Down Expand Up @@ -33,7 +34,8 @@ public final class RuntimeOptions implements
io.cucumber.core.runner.Options,
io.cucumber.core.plugin.Options,
io.cucumber.core.filter.Options,
io.cucumber.core.backend.Options {
io.cucumber.core.backend.Options,
io.cucumber.core.eventbus.Options {

private final List<URI> glue = new ArrayList<>();
private final List<Expression> tagExpressions = new ArrayList<>();
Expand All @@ -48,6 +50,7 @@ public final class RuntimeOptions implements
private PickleOrder pickleOrder = StandardPickleOrders.lexicalUriOrder();
private int count = 0;
private Class<? extends ObjectFactory> objectFactoryClass;
private Class<? extends UuidGenerator> uuidGeneratorClass;
private String publishToken;
private boolean publish;
private boolean publishQuiet;
Expand Down Expand Up @@ -158,6 +161,15 @@ void setObjectFactoryClass(Class<? extends ObjectFactory> objectFactoryClass) {
this.objectFactoryClass = objectFactoryClass;
}

@Override
public Class<? extends UuidGenerator> getUuidGeneratorClass() {
return uuidGeneratorClass;
}

void setUuidGeneratorClass(Class<? extends UuidGenerator> uuidGeneratorClass) {
this.uuidGeneratorClass = uuidGeneratorClass;
}

void setSnippetType(SnippetType snippetType) {
this.snippetType = snippetType;
}
Expand Down
Loading

0 comments on commit 5691d30

Please sign in to comment.