Skip to content

Commit

Permalink
#323: Improved CLI performance by storing intermediate CLI tree state…
Browse files Browse the repository at this point in the history
… snapshot as serialized data (using Kryo) and reading that back instead of rebuilding the tree from scratch (saves 1 seconds or roughly 25% of total time)
  • Loading branch information
bbottema committed Jun 19, 2021
1 parent 57a2398 commit 6159983
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 11 deletions.
14 changes: 14 additions & 0 deletions modules/cli-module/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,20 @@
<optional>true</optional>
</dependency>

<!-- needed for converting the CLI options structure to binary format for quicker CLI startup -->
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>5.0.0-RC1</version>
<scope>compile</scope>
</dependency>
<dependency><!-- adds support for deserializing UnmodifiableCollection -->
<groupId>de.javakaffee</groupId>
<artifactId>kryo-serializers</artifactId>
<version>0.45</version>
<scope>compile</scope>
</dependency>

<!-- Logging -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.simplejavamail.internal.clisupport;

import java.io.File;
import java.net.URISyntaxException;
import java.net.URL;

/**
* This helper makes sure we can load cli.data no matter how the code is run (from mvn, intellij, command line).
*/
public class CliDataLocator {
public static String determinelocateCLIDataFile() {
// the following is needed bacause this is a project with submodules
// and it changes depending on how the code is executed
if (new File("src/test/resources/log4j2.xml").exists()) {
return "src/main/resources/cli.data";
} else if (new File("modules/cli-module/src/test/resources/log4j2.xml").exists()) {
return "modules/cli-module/src/main/resources/cli.data";
} else {
URL resource = CliDataLocator.class.getClassLoader().getResource("log4j2.xml");
if (resource != null) {
try {
return new File(resource.toURI()).getParentFile().getPath() + "/cli.data";
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
throw new AssertionError("Was unable to locate resources folder. Did you delete log4j2.xml?");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,43 +1,68 @@
package org.simplejavamail.internal.clisupport;

import org.jetbrains.annotations.TestOnly;
import org.simplejavamail.api.email.EmailStartingBuilder;
import org.simplejavamail.api.internal.clisupport.model.CliDeclaredOptionSpec;
import org.simplejavamail.api.internal.clisupport.model.CliReceivedCommand;
import org.simplejavamail.api.mailer.MailerFromSessionBuilder;
import org.simplejavamail.api.mailer.MailerRegularBuilder;
import org.simplejavamail.internal.clisupport.serialization.SerializationUtil;
import org.simplejavamail.internal.util.MiscUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import picocli.CommandLine;
import picocli.CommandLine.ParseResult;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import static org.simplejavamail.internal.clisupport.BuilderApiToPicocliCommandsMapper.generateOptionsFromBuilderApi;
import static org.simplejavamail.internal.clisupport.CliCommandLineProducer.configurePicoCli;
import static org.simplejavamail.internal.clisupport.CliExecutionException.ERROR_INVOKING_BUILDER_API;

public class CliSupport {

private static final Logger LOGGER = LoggerFactory.getLogger(CliSupport.class);

private static final int CONSOLE_TEXT_WIDTH = 150;

private static final Class<?>[] RELEVANT_BUILDER_ROOT_API = {EmailStartingBuilder.class, MailerRegularBuilder.class, MailerFromSessionBuilder.class};
private static final List<CliDeclaredOptionSpec> DECLARED_OPTIONS = generateOptionsFromBuilderApi(RELEVANT_BUILDER_ROOT_API);
private static final CommandLine PICOCLI_COMMAND_LINE = configurePicoCli(DECLARED_OPTIONS, CONSOLE_TEXT_WIDTH);

private static final File CLI_DATAFILE = new File(CliDataLocator.determinelocateCLIDataFile());

private static final Class<?>[] RELEVANT_BUILDER_ROOT_API = new Class[] { EmailStartingBuilder.class, MailerRegularBuilder.class, MailerFromSessionBuilder.class };
private static final List<CliDeclaredOptionSpec> DECLARED_OPTIONS = produceCliDeclaredOptionSpec();
private static final CommandLine PICOCLI_COMMAND_LINE = configurePicoCli(DECLARED_OPTIONS, CONSOLE_TEXT_WIDTH);

private static List<CliDeclaredOptionSpec> produceCliDeclaredOptionSpec() {
try {
if (!CLI_DATAFILE.exists()) {
LOGGER.info("Initial cli.data not found, writing to (one time action): {}", CLI_DATAFILE);
List<CliDeclaredOptionSpec> serializable = generateOptionsFromBuilderApi(RELEVANT_BUILDER_ROOT_API);
MiscUtil.writeFileBytes(CLI_DATAFILE, SerializationUtil.serialize(serializable));
}
return SerializationUtil.deserialize(MiscUtil.readFileBytes(CLI_DATAFILE));
} catch (IOException e) {
throw new CliExecutionException(ERROR_INVOKING_BUILDER_API, e);
}
}

public static void runCLI(String[] args) {
ParseResult pr = PICOCLI_COMMAND_LINE.parseArgs(cutOffAtHelp(args));

if (!CliCommandLineConsumerUsageHelper.processAndApplyHelp(pr, CONSOLE_TEXT_WIDTH)) {
CliReceivedCommand cliReceivedOptionData = CliCommandLineConsumer.consumeCommandLineInput(pr, DECLARED_OPTIONS);
CliCommandLineConsumerResultHandler.processCliResult(cliReceivedOptionData);
}
}


@TestOnly
public static void listUsagesForAllOptions() {
for (CliDeclaredOptionSpec declaredOption : DECLARED_OPTIONS) {
runCLI(new String[]{"send", declaredOption.getName() + "--help"});
runCLI(new String[] { "send", declaredOption.getName() + "--help" });
System.out.print("\n\n\n");
}
}

/**
* This is needed to avoid picocli to error out on other --params that are misconfigured.
*/
Expand All @@ -49,11 +74,11 @@ private static String[] cutOffAtHelp(String[] args) {
break;
}
}

if (argsToKeep.isEmpty()) {
argsToKeep.add("--help");
}

return argsToKeep.toArray(new String[]{});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.simplejavamail.internal.clisupport.serialization;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.esotericsoftware.kryo.serializers.FieldSerializer;
import com.esotericsoftware.kryo.serializers.JavaSerializer;
import com.esotericsoftware.kryo.util.DefaultInstantiatorStrategy;
import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer;
import org.jetbrains.annotations.NotNull;
import org.objenesis.strategy.StdInstantiatorStrategy;

import java.io.IOException;

/**
* Used to serialize attachments of nested Outlook messages. This is needed because outlook-message-parser returns a Java structure from a .msg source, but this conversion is 1-way. An Email object
* represents attachments as DataSources however, so for this we need to serialize back from the java structure to a binary format. This Util does this.
* <br>
* Then for users to obtain the Javastructure again, they must use this util to deserialize the relevant attachment.
*
* @see <a href="https://github.com/bbottema/simple-java-mail/issues/298">GitHub issue #314</a>
*/
public class SerializationUtil {

private static final Kryo KRYO = initKryo();

@NotNull
private static Kryo initKryo() {
Kryo kryo = new Kryo();
kryo.setRegistrationRequired(false);
kryo.setInstantiatorStrategy(new DefaultInstantiatorStrategy(new StdInstantiatorStrategy()));
// final FieldSerializer.FieldSerializerConfig config = new FieldSerializer.FieldSerializerConfig();
// config.setSerializeTransient(true);
UnmodifiableCollectionsSerializer.registerSerializers(kryo);
return kryo;
}

private static <T> void registerClassSerializer(final Kryo kryo, final FieldSerializer.FieldSerializerConfig config, Class<T> type) {
kryo.register(type, new FieldSerializer<T>(kryo, type, config));
}

private static <T> void registerClassJavaSerializer(final Kryo kryo, Class<T> type) {
kryo.register(type, new JavaSerializer());
}

@NotNull
public static byte[] serialize(@NotNull final Object serializable)
throws IOException {
final Output output = new Output(1024, -1);
KRYO.writeClassAndObject(output, serializable);
return output.toBytes();
}

@SuppressWarnings("unchecked")
@NotNull
public static <T> T deserialize(@NotNull final byte[] serialized)
throws IOException {
return (T) KRYO.readClassAndObject(new Input(serialized));
}
}

0 comments on commit 6159983

Please sign in to comment.