From 4a97e2ec1c7c6f0b1ad4ed63b84ceebe78895cbb Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Fri, 4 Sep 2026 16:01:56 +0100 Subject: [PATCH 1/3] Remove explicit GC before process launch Introduced in 530a468a as an experiment to reduce fork memory. Measured at 50s of GC pauses in a 98s test run vs 0.14s with automatic GC only. Automatic GC still runs; removing this does not disable collection. --- SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java b/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java index 13c0c9a7..49f767e0 100644 --- a/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java +++ b/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java @@ -190,15 +190,7 @@ public static ProcessOutput runProcess(ProcessBuilder builder, Process process; try { - // Experiment: Calling Garbage Collector before starting process in order to - // reduce memory required to fork VM - // http://www.bryanmarty.com/2012/01/14/forking-jvm/ - long totalMemBefore = Runtime.getRuntime().totalMemory(); - System.gc(); - long totalMemAfter = Runtime.getRuntime().totalMemory(); - SpecsLogs.msgLib("Preparing to run process, memory before -> after GC: " - + SpecsStrings.parseSize(totalMemBefore) + " -> " + SpecsStrings.parseSize(totalMemAfter)); - process = builder.start(); + process = builder.start(); } catch (IOException e) { throw new RuntimeException("Could not start process", e); From b54f698740a84c6a0df677442ccb4623e48ac78a Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Fri, 4 Sep 2026 17:07:56 +0100 Subject: [PATCH 2/3] Launch processes with direct argv on Linux Drop the bash -l -c wrapper added in 44d1d938. The wrapper added ~50ms per launch for shell parsing, broke argument fidelity (naive space escaping), and re-sourced login profiles over the inherited environment instead of carrying it faithfully. ProcessBuilder children already inherit the JVM's full environment, user, and working directory. All in-tree callers pass plain argv; shell semantics for string commands are no longer supported on Linux. --- SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java b/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java index 49f767e0..f691a87e 100644 --- a/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java +++ b/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java @@ -246,19 +246,6 @@ private static void processCommand(ProcessBuilder builder) { newCommand.add("/c"); newCommand.addAll(builder.command()); - builder.command(newCommand); - } else if (isLinux()) { - // Update command - List newCommand = new ArrayList<>(4); - newCommand.add("bash"); - // Same user - newCommand.add("-l"); - // Command - newCommand.add("-c"); - newCommand.add(builder.command().stream() - .map(arg -> arg.replace(" ", "\\ ")) - .collect(Collectors.joining(" "))); - builder.command(newCommand); } From 44ab19744fd4902f02ed85e1079e6214ee00dfef Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Sat, 5 Sep 2026 02:05:31 +0100 Subject: [PATCH 3/3] Handle failed process launches at the call sites With direct argv, a missing or non-executable command now throws from runProcess(), as its javadoc always documented ('If there is any problem with the process, throws an exception') - the removed shell wrapper used to mask launch failures as exit code 127. Handle the launch failure where graceful failure is the intent: - SpecsGraphviz: dot being absent is a normal condition, so checkDot() returns false and renderDot() logs instead of crashing. - ProcessExecution: the jobs framework is built on return codes with no exception handling, so run() reports the failed launch as exit code 127 instead of propagating. This also restores SpecsSystem.isCommandAvailable()'s intended behavior: it detects missing commands by catching the launch exception, which the wrapper's masking previously defeated. --- .../pt/up/fe/specs/util/SpecsGraphviz.java | 25 +++++++++++++++---- .../src/pt/up/fe/specs/util/SpecsSystem.java | 20 +++++++++++++++ .../util/jobs/execution/ProcessExecution.java | 16 +++++++++++- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/SpecsUtils/src/pt/up/fe/specs/util/SpecsGraphviz.java b/SpecsUtils/src/pt/up/fe/specs/util/SpecsGraphviz.java index d0d6b3da..120242a6 100644 --- a/SpecsUtils/src/pt/up/fe/specs/util/SpecsGraphviz.java +++ b/SpecsUtils/src/pt/up/fe/specs/util/SpecsGraphviz.java @@ -29,8 +29,13 @@ public class SpecsGraphviz { private static final Lazy IS_DOT_AVAILABLE = Lazy.newInstance(SpecsGraphviz::checkDot); private static boolean checkDot() { - var result = SpecsSystem.runProcess(Arrays.asList("dot", "-?"), false, false); - return result.getReturnValue() == 0; + try { + var result = SpecsSystem.runProcess(Arrays.asList("dot", "-?"), false, false); + return result.getReturnValue() == 0; + } catch (RuntimeException e) { + // The launch itself failed, e.g. Graphviz is not installed + return false; + } } public static boolean isDotAvailable() { @@ -47,9 +52,19 @@ public static void renderDot(File dotFile, DotRenderFormat format, File outputFi var command = Arrays.asList("dot", format.getFlag(), dotFile.getAbsolutePath(), "-o", outputFile.getAbsolutePath()); - var result = SpecsSystem.runProcess(command, false, false); - if (result.getReturnValue() == 0) { - SpecsLogs.debug(() -> "Rendered dot file '" + dotFile.getAbsolutePath() + "' as " + format); + try { + var result = SpecsSystem.runProcess(command, false, false); + if (result.getReturnValue() == 0) { + SpecsLogs.debug(() -> "Rendered dot file '" + dotFile.getAbsolutePath() + "' as " + format); + } + } catch (RuntimeException e) { + // Only a missing Graphviz installation is skipped; any other + // failure is rethrown + if (!SpecsSystem.isLaunchFailure(e)) { + throw e; + } + + SpecsLogs.msgInfo("Graphviz not available, could not render dot file '" + dotFile.getAbsolutePath() + "'"); } } diff --git a/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java b/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java index f691a87e..cd4400b3 100644 --- a/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java +++ b/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java @@ -578,6 +578,26 @@ public static boolean isCommandAvailable(List command, File workingdir) } + /** + * Checks if the throwable was caused by a failure to launch the process + * itself (e.g., command not found or not executable), as opposed to a + * failure after a successful launch. + * + * @param throwable + * @return + */ + public static boolean isLaunchFailure(Throwable throwable) { + for (Throwable currentThrowable = throwable; currentThrowable != null; currentThrowable = currentThrowable + .getCause()) { + if (currentThrowable instanceof IOException) { + return true; + } + } + + return false; + } + + /** * Adds a path to the java.library.path property, and flushes the path cache so * that subsequent System.load calls diff --git a/SpecsUtils/src/pt/up/fe/specs/util/jobs/execution/ProcessExecution.java b/SpecsUtils/src/pt/up/fe/specs/util/jobs/execution/ProcessExecution.java index 8c926594..5ae0ddfa 100644 --- a/SpecsUtils/src/pt/up/fe/specs/util/jobs/execution/ProcessExecution.java +++ b/SpecsUtils/src/pt/up/fe/specs/util/jobs/execution/ProcessExecution.java @@ -16,6 +16,7 @@ import java.io.File; import java.util.List; +import pt.up.fe.specs.util.SpecsLogs; import pt.up.fe.specs.util.SpecsSystem; /** @@ -45,7 +46,20 @@ public ProcessExecution(List commandArgs, String workingFoldername) { */ @Override public int run() { - return SpecsSystem.run(this.commandArgs, new File(this.workingFoldername)); + try { + return SpecsSystem.run(this.commandArgs, new File(this.workingFoldername)); + } catch (RuntimeException e) { + // Only a launch failure due to a missing command maps to a return + // code (the shell convention of 127, 'command not found'). Any + // other failure is not representable as an exit code and is + // rethrown, as the process may even have been running. + if (!SpecsSystem.isLaunchFailure(e)) { + throw e; + } + + SpecsLogs.msgInfo("Could not run command '" + getCommandString() + "': " + e.getMessage()); + return 127; + } } @Override