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

Amortize slow LFF startup time #1788

Merged
merged 8 commits into from
May 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@ spotless {
endWithNewline()
}

// format 'linguaFranca', {
// addStep(LfFormatStep.create(project.projectDir))
// target 'test/*/src/**/*.lf' // you have to set the target manually
// targetExclude 'test/**/failing/**'
// }

format 'linguaFranca', {
addStep(LfFormatStep.create())
target 'test/*/src/**/*.lf' // you have to set the target manually
targetExclude 'test/**/failing/**'
}
}

//spotlessLinguaFranca.dependsOn(":org.lflang:cli:lff:installDist")

// Make the LF formatting task depend on lff
spotlessLinguaFranca.dependsOn(':org.lflang:cli:lff:installDist')
spotlessLinguaFranca.inputs.files(tasks.getByPath(':org.lflang:cli:lff:installDist').outputs)

distributions {
clitools {
Expand Down
92 changes: 62 additions & 30 deletions buildSrc/src/main/java/lfformat/LfFormatStep.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@

import com.diffplug.spotless.FormatterStep;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.net.URL;
import java.net.URLClassLoader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;

/**
* {@code LfFormatStep} is used by the Spotless Gradle plugin as a custom formatting step for
Expand All @@ -21,38 +19,52 @@ public final class LfFormatStep {
private LfFormatStep() {}

/** Return a {@code FormatterStep} for LF code. */
public static FormatterStep create(File projectRoot) {
Step.projectRoot = projectRoot.toPath();
public static FormatterStep create() throws IOException, InterruptedException {
return new Step();
}

/** Implement LF-specific formatting functionality. */
public static class Step implements FormatterStep, Serializable {
private static class Step implements FormatterStep {
// The use of the static keyword here is a workaround for serialization difficulties.
/** The path to the lingua-franca repository. */
private static Path projectRoot;

private static Process formatter;
private static Writer writer;

private static BufferedReader reader;
private static BufferedReader error;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a little bit of a red flag that these variables are static. It is necessary because Step needs to be Serializable. I do not fully understand why this is required by Spotless, but I am under the impression that it relates to an optimization that we are not trying to use at the moment.


private Step() throws IOException, InterruptedException {
terminateFormatter();
}

@Override
public String format(
@SuppressWarnings("NullableProblems") String rawUnix,
@SuppressWarnings("NullableProblems") File file)
public String format(@SuppressWarnings("NullableProblems") String rawUnix, File file)
throws IOException, InterruptedException {
Process p = runFormatter(file);
initializeFormatter();
StringBuilder output = new StringBuilder();
BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
output.append(line).append("\n");
try {
writer.write(file.getAbsoluteFile().toString().strip() + "\n");
writer.flush();
} catch (IOException e) {
formatter.waitFor();
error.lines().forEach(System.out::println);
formatter = null;
initializeFormatter();
throw new RuntimeException("Failed to format " + file + ".\nPlease ensure that this file passes validator checks.");
}
int returnCode = p.waitFor();
if (returnCode != 0) {
throw new RuntimeException("Failed to reformat file. Exit code: " + returnCode + " Output: " + output.toString());
String line = reader.readLine();
while (line != null && !line.endsWith("\0")) {
if (!output.isEmpty()) output.append("\n");
output.append(line);
line = reader.readLine();
}
return output.toString();
}

/** Run the formatter on the given file and return the resulting process handle. */
private Process runFormatter(File file) throws IOException {
/** Idempotently initialize the formatter. */
private static void initializeFormatter() throws IOException {
if (formatter != null) return;
final Path lffPath =
Path.of(
"org.lflang",
Expand All @@ -63,15 +75,35 @@ private Process runFormatter(File file) throws IOException {
"lff",
"bin",
"lff");
// It looks silly to invoke Java from Java, but it is necessary in
// order to break the circularity of needing the program to be built
// in order for it to be built.
return new ProcessBuilder(
List.of(
lffPath.toString(),
"--dry-run",
file.getAbsoluteFile().toString()))
formatter = new ProcessBuilder(
List.of(
lffPath.toString(),
"--dry-run",
"--stdin"))
.start();
writer = new BufferedWriter(new OutputStreamWriter(formatter.getOutputStream()));
reader = new BufferedReader(new InputStreamReader(formatter.getInputStream()));
error = new BufferedReader(new InputStreamReader(formatter.getErrorStream()));
Runtime.getRuntime()
.addShutdownHook(
new Thread(
() -> {
try {
terminateFormatter();
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
}));
}

/** Idempotently trigger a graceful termination of the formatter. */
private static void terminateFormatter() throws IOException, InterruptedException {
if (formatter == null) return;
writer.close();
formatter.waitFor();
reader.close();
error.close();
formatter = null;
}

@SuppressWarnings("NullableProblems")
Expand Down
28 changes: 26 additions & 2 deletions org.lflang/cli/base/src/main/java/org/lflang/cli/CliBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Provider;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
Expand Down Expand Up @@ -62,6 +66,9 @@ static class MutuallyExclusive {

@Option(names = "--json-file", description = "JSON file containing CLI arguments.")
private Path jsonFile;

@Option(names = "--stdin", description = "Read paths to Lingua Franca programs from stdin.")
private boolean stdin;
}

@ArgGroup(exclusive = true, multiplicity = "1")
Expand Down Expand Up @@ -164,8 +171,21 @@ protected Path toAbsolutePath(Path other) {
* @return Validated input paths.
*/
protected List<Path> getInputPaths() {
List<Path> paths =
topLevelArg.files.stream().map(io.getWd()::resolve).collect(Collectors.toList());
List<Path> paths;
if (topLevelArg.stdin) {
var input = new BufferedInputStream(System.in);
var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
String line;
try {
line = reader.readLine();
} catch (IOException e) {
throw new RuntimeException(e);
}
if (line == null) return List.of();
return List.of(Path.of(line));
} else {
paths = topLevelArg.files.stream().map(io.getWd()::resolve).collect(Collectors.toList());
}

for (Path path : paths) {
if (!Files.exists(path)) {
Expand All @@ -176,6 +196,10 @@ protected List<Path> getInputPaths() {
return paths;
}

protected final boolean stdinMode() {
return topLevelArg.stdin;
}

/**
* Returns the validated, normalized output path.
*
Expand Down
28 changes: 16 additions & 12 deletions org.lflang/cli/lff/src/main/java/org/lflang/cli/Lff.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,20 +80,23 @@ public static void main(Io io, final String... args) {
/** Validates all paths and invokes the formatter on the input paths. */
@Override
public void doRun() {
List<Path> paths = getInputPaths();
final Path outputRoot = getOutputRoot();
List<Path> paths;
do {
paths = getInputPaths();
final Path outputRoot = getOutputRoot();

try {
// Format all files defined by the list of paths.
formatAllFiles(paths, outputRoot);
try {
// Format all files defined by the list of paths.
formatAllFiles(paths, outputRoot);

exitIfCollectedErrors();
if (!dryRun || verbose) {
reporter.printInfo("Done formatting.");
exitIfCollectedErrors();
if (!dryRun || verbose) {
reporter.printInfo("Done formatting.");
}
} catch (RuntimeException e) {
reporter.printFatalErrorAndExit("An unexpected error occurred:", e);
}
} catch (RuntimeException e) {
reporter.printFatalErrorAndExit("An unexpected error occurred:", e);
}
} while (stdinMode() && !paths.isEmpty());
}

/*
Expand Down Expand Up @@ -155,7 +158,8 @@ private void formatSingleFile(Path path, Path inputRoot, Path outputRoot) {
FormattingUtils.render(resource.getContents().get(0), lineLength);

if (dryRun) {
io.getOut().print(formattedFileContents);
io.getOut().println(formattedFileContents);
io.getOut().println("\0");
} else {
try {
FileUtil.writeToFile(formattedFileContents, outputPath, true);
Expand Down