diff --git a/src/it/java/io/jawk/site/CompatibilitySummaryGenerator.java b/src/it/java/io/jawk/site/CompatibilitySummaryGenerator.java
index a77654d..26e71c5 100644
--- a/src/it/java/io/jawk/site/CompatibilitySummaryGenerator.java
+++ b/src/it/java/io/jawk/site/CompatibilitySummaryGenerator.java
@@ -4,7 +4,7 @@
* ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
* Jawk
* ჻჻჻჻჻჻
- * Copyright 2006 - 2026 MetricsHub
+ * Copyright (C) 2006 - 2026 MetricsHub
* ჻჻჻჻჻჻
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
diff --git a/src/main/java/io/jawk/Awk.java b/src/main/java/io/jawk/Awk.java
index 3bcb308..bf32700 100644
--- a/src/main/java/io/jawk/Awk.java
+++ b/src/main/java/io/jawk/Awk.java
@@ -310,6 +310,17 @@ public AVM createAvm() {
return createAvm(this.settings);
}
+ /**
+ * Creates a reusable runtime backed by one {@link AVM} instance, optionally
+ * collecting runtime profiling statistics.
+ *
+ * @param profilingEnabled whether runtime profiling should be enabled
+ * @return reusable AVM
+ */
+ public AVM createAvm(boolean profilingEnabled) {
+ return createAvm(this.settings, profilingEnabled);
+ }
+
/**
* Starts building a run request for a compiled AWK program.
*
@@ -680,7 +691,19 @@ public AVM prepareEval(InputSource source) throws IOException {
* @return reusable AVM
*/
protected AVM createAvm(AwkSettings settingsParam) {
- return new AVM(settingsParam, this.extensionInstances);
+ return createAvm(settingsParam, false);
+ }
+
+ /**
+ * Creates an {@link AVM} using the provided runtime settings and profiling
+ * mode.
+ *
+ * @param settingsParam runtime settings to apply
+ * @param profilingEnabled whether runtime profiling should be enabled
+ * @return reusable AVM
+ */
+ protected AVM createAvm(AwkSettings settingsParam, boolean profilingEnabled) {
+ return new AVM(settingsParam, this.extensionInstances, profilingEnabled);
}
/**
diff --git a/src/main/java/io/jawk/Cli.java b/src/main/java/io/jawk/Cli.java
index 0da3c2e..e186f88 100644
--- a/src/main/java/io/jawk/Cli.java
+++ b/src/main/java/io/jawk/Cli.java
@@ -34,6 +34,7 @@
import java.io.ObjectOutputStream;
import java.io.PrintStream;
import java.io.StringReader;
+import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
@@ -92,6 +93,8 @@ public final class Cli {
private boolean printUsage;
private boolean sandbox;
private boolean disableOptimize;
+ private boolean profiling;
+ private File profilingOutputFile;
private File persistentMemoryFile;
/**
@@ -163,6 +166,15 @@ public boolean isDisableOptimize() {
return disableOptimize;
}
+ /**
+ * Indicates whether runtime profiling was requested.
+ *
+ * @return {@code true} when profiling should be enabled
+ */
+ public boolean isProfiling() {
+ return profiling;
+ }
+
/**
* Returns the list of script sources specified on the command line.
*
@@ -276,6 +288,17 @@ public void parse(String[] args) {
} else if (arg.equals("-s") || arg.equals("--no-optimize")) {
// -s/--no-optimize : skip tuple queue optimizations
disableOptimize = true;
+ } else if (arg.equals("--profile")) {
+ // --profile : collect and print runtime profiling statistics
+ profiling = true;
+ } else if (arg.startsWith("--profile=")) {
+ // --profile=filename : collect profiling statistics and write them to a file
+ String file = arg.substring("--profile=".length());
+ if (file.length() == 0) {
+ throw new IllegalArgumentException("Need output filename for --profile");
+ }
+ profiling = true;
+ profilingOutputFile = new File(file);
} else if (arg.equals("--dump-intermediate")) {
// --dump-intermediate : dump intermediate tuples to file
dumpIntermediateCode = true;
@@ -470,7 +493,7 @@ private File resolvePersistentMemoryFile() {
*/
private void executeProgram(Awk awk, AwkProgram program, File memoryFile) throws Exception {
OutputStreamAwkSink sink = new OutputStreamAwkSink(out, settings.getLocale());
- try (AVM avm = awk.createAvm()) {
+ try (AVM avm = awk.createAvm(profiling)) {
avm.setAwkSink(sink);
avm.setErrorStream(err);
if (memoryFile != null) {
@@ -493,7 +516,36 @@ private void executeProgram(Awk awk, AwkProgram program, File memoryFile) throws
if (memoryFile != null) {
savePersistentMemory(avm, memoryFile);
}
+ if (profiling) {
+ printProfilingReport(avm);
+ }
+ }
+ }
+ }
+
+ private void printProfilingReport(AVM avm) {
+ if (profilingOutputFile == null) {
+ err.println();
+ avm.getProfilingReport().print(err);
+ return;
+ }
+ File parent = profilingOutputFile.getAbsoluteFile().getParentFile();
+ if (parent != null && !parent.isDirectory() && !parent.mkdirs() && !parent.isDirectory()) {
+ String message = "Failed to create directory '" + parent + "' for profiling report.";
+ throw new UncheckedIOException(message, new IOException(message));
+ }
+ try (PrintStream profileOut = new PrintStream(profilingOutputFile, "UTF-8")) {
+ avm.getProfilingReport().print(profileOut);
+ profileOut.flush();
+ if (profileOut.checkError()) {
+ throw new UncheckedIOException(
+ "Failed to write profiling report '" + profilingOutputFile + "'.",
+ new IOException("PrintStream reported an error"));
}
+ } catch (IOException ex) {
+ throw new UncheckedIOException(
+ "Failed to write profiling report '" + profilingOutputFile + "': " + ex.getMessage(),
+ ex);
}
}
@@ -571,6 +623,7 @@ private static void usage(PrintStream dest) {
" [--dump-syntax]" +
" [--dump-intermediate]" +
" [-s|--no-optimize]" +
+ " [--profile[=filename]]" +
" [--locale locale]" +
" [-t]" +
" [-l extension]..." +
@@ -605,6 +658,9 @@ private static void usage(PrintStream dest) {
dest.println(" --dump-syntax = Print the syntax tree.");
dest.println(" --dump-intermediate = Print the intermediate code.");
dest.println(" -s, --no-optimize = (extension) Disable optimizations during compilation.");
+ dest
+ .println(
+ " --profile[=filename] = (extension) Print tuple and function execution statistics to stderr or file.");
dest.println(" --locale Locale = (extension) Specify a locale to be used instead of US-English");
dest.println(" --list-ext = (extension) List available extensions.");
dest.println();
@@ -665,6 +721,9 @@ public static void main(String[] args) {
System.err.println("Failed to parse arguments. Please see the help/usage output (cmd line switch '-h').");
e.printStackTrace(System.err);
System.exit(1);
+ } catch (UncheckedIOException e) {
+ System.err.printf("%s: %s%n", e.getClass().getSimpleName(), e.getMessage());
+ System.exit(1);
} catch (Exception e) {
System.err.printf("%s: %s%n", e.getClass().getSimpleName(), e.getMessage());
System.exit(1);
diff --git a/src/main/java/io/jawk/SandboxedAwk.java b/src/main/java/io/jawk/SandboxedAwk.java
index ae1483f..a9a2c2b 100644
--- a/src/main/java/io/jawk/SandboxedAwk.java
+++ b/src/main/java/io/jawk/SandboxedAwk.java
@@ -96,9 +96,19 @@ public AVM createAvm() {
return createAvm(getSettings());
}
+ @Override
+ public AVM createAvm(boolean profilingEnabled) {
+ return createAvm(getSettings(), profilingEnabled);
+ }
+
@Override
protected AVM createAvm(AwkSettings settingsParam) {
- return new SandboxedAVM(settingsParam, getExtensionInstances());
+ return createAvm(settingsParam, false);
+ }
+
+ @Override
+ protected AVM createAvm(AwkSettings settingsParam, boolean profilingEnabled) {
+ return new SandboxedAVM(settingsParam, getExtensionInstances(), profilingEnabled);
}
}
diff --git a/src/main/java/io/jawk/backend/AVM.java b/src/main/java/io/jawk/backend/AVM.java
index ae2a20f..d84a815 100644
--- a/src/main/java/io/jawk/backend/AVM.java
+++ b/src/main/java/io/jawk/backend/AVM.java
@@ -133,6 +133,10 @@ private void push(Object o) {
}
private final AwkSettings settings;
+ private final boolean profiling;
+ private final Map tupleProfilingStats;
+ private final Map functionProfilingStats;
+ private final Deque activeProfilingFunctions;
private boolean inputSourceFilelistAssignmentsApplied;
private InputSource resolvedInputSource;
private AwkExpression installedEvalExpression;
@@ -158,9 +162,34 @@ public AVM() {
*/
public AVM(final AwkSettings parameters,
final Map extensionInstances) {
+ this(parameters, extensionInstances, false);
+ }
+
+ /**
+ * Construct the interpreter, optionally enabling runtime profiling.
+ *
+ * @param parameters The parameters affecting the behavior of the
+ * interpreter.
+ * @param extensionInstances Map of the extensions to load
+ * @param profilingEnabled Whether to collect profiling statistics
+ */
+ public AVM(
+ final AwkSettings parameters,
+ final Map extensionInstances,
+ final boolean profilingEnabled) {
this.settings = parameters != null ? parameters : AwkSettings.DEFAULT_SETTINGS;
this.extensionInstances = extensionInstances == null ?
Collections.emptyMap() : extensionInstances;
+ this.profiling = profilingEnabled;
+ if (profilingEnabled) {
+ this.tupleProfilingStats = new java.util.EnumMap(Opcode.class);
+ this.functionProfilingStats = new LinkedHashMap();
+ this.activeProfilingFunctions = new ArrayDeque();
+ } else {
+ this.tupleProfilingStats = null;
+ this.functionProfilingStats = null;
+ this.activeProfilingFunctions = null;
+ }
arguments = Collections.emptyList();
sortedArrayKeys = this.settings.isUseSortedArrayKeys();
@@ -975,10 +1004,14 @@ private void executeTuples(PositionTracker position)
IOException {
Map conditionPairs = null;
Opcode opcode = null;
+ long tupleStartNanos = 0L;
try {
while (!position.isEOF()) {
// System_out.println("--> "+position);
opcode = position.opcode();
+ if (profiling) {
+ tupleStartNanos = beforeProfiledTuple(position, opcode);
+ }
// switch on OPCODE
switch (opcode) {
case PRINT: {
@@ -2217,7 +2250,8 @@ private void executeTuples(PositionTracker position)
if (!position.classArg().isInstance(o)) {
throw new AwkRuntimeException(
position.lineNumber(),
- "Verification failed. Top-of-stack = " + o.getClass() + " isn't an instance of " + position.classArg());
+ "Verification failed. Top-of-stack = " + o.getClass() + " isn't an instance of "
+ + position.classArg());
}
push(o);
position.next();
@@ -2811,8 +2845,16 @@ private void executeTuples(PositionTracker position)
default:
throw new Error("invalid opcode: " + position.opcode());
}
+ if (profiling) {
+ afterProfiledTuple(opcode, tupleStartNanos);
+ }
}
+ } catch (ExitException ee) {
+ if (profiling && (opcode == Opcode.EXIT_WITH_CODE || opcode == Opcode.EXIT_WITHOUT_CODE)) {
+ afterProfiledTuple(opcode, tupleStartNanos);
+ }
+ throw ee;
} catch (IOException ioe) {
// clear runtime stack
runtimeStack.popAllFrames();
@@ -2842,6 +2884,86 @@ private void executeTuples(PositionTracker position)
}
}
+ /**
+ * Clears all collected profiling statistics.
+ */
+ public void resetProfiling() {
+ if (!profiling) {
+ return;
+ }
+ tupleProfilingStats.clear();
+ functionProfilingStats.clear();
+ activeProfilingFunctions.clear();
+ }
+
+ /**
+ * Returns an immutable snapshot of the collected profiling statistics.
+ *
+ * @return profiling report snapshot
+ */
+ public ProfilingReport getProfilingReport() {
+ if (!profiling) {
+ return ProfilingReport.empty();
+ }
+ return new ProfilingReport(tupleProfilingStats, functionProfilingStats);
+ }
+
+ private long beforeProfiledTuple(PositionTracker position, Opcode opcode) {
+ long now = System.nanoTime();
+ if (opcode == Opcode.CALL_FUNCTION) {
+ activeProfilingFunctions.push(new ActiveFunction(position.stringArg(1), now));
+ } else if (opcode == Opcode.EXTENSION) {
+ ExtensionFunction function = position.extensionFunctionArg();
+ activeProfilingFunctions.push(new ActiveFunction(function.getKeyword(), now));
+ }
+ return now;
+ }
+
+ private void afterProfiledTuple(Opcode opcode, long tupleStartNanos) {
+ long now = System.nanoTime();
+ statisticsFor(tupleProfilingStats, opcode).add(now - tupleStartNanos);
+ if (opcode == Opcode.EXIT_WITH_CODE || opcode == Opcode.EXIT_WITHOUT_CODE) {
+ recordAllFunctionExits(now);
+ } else if (opcode == Opcode.EXTENSION || opcode == Opcode.RETURN_FROM_FUNCTION) {
+ recordFunctionExit(now);
+ }
+ }
+
+ private static ProfilingReport.Accumulator statisticsFor(
+ Map stats,
+ K key) {
+ ProfilingReport.Accumulator accumulator = stats.get(key);
+ if (accumulator == null) {
+ accumulator = new ProfilingReport.Accumulator();
+ stats.put(key, accumulator);
+ }
+ return accumulator;
+ }
+
+ private void recordFunctionExit(long now) {
+ if (activeProfilingFunctions.isEmpty()) {
+ return;
+ }
+ ActiveFunction function = activeProfilingFunctions.pop();
+ statisticsFor(functionProfilingStats, function.name).add(now - function.startNanos);
+ }
+
+ private void recordAllFunctionExits(long now) {
+ while (!activeProfilingFunctions.isEmpty()) {
+ recordFunctionExit(now);
+ }
+ }
+
+ private static final class ActiveFunction {
+ private final String name;
+ private final long startNanos;
+
+ private ActiveFunction(String name, long startNanos) {
+ this.name = name;
+ this.startNanos = startNanos;
+ }
+ }
+
/**
* Releases any prepared input source and runtime I/O resources owned by this
* AVM.
diff --git a/src/main/java/io/jawk/backend/ProfilingReport.java b/src/main/java/io/jawk/backend/ProfilingReport.java
new file mode 100644
index 0000000..8dd2bb7
--- /dev/null
+++ b/src/main/java/io/jawk/backend/ProfilingReport.java
@@ -0,0 +1,198 @@
+package io.jawk.backend;
+
+/*-
+ * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
+ * Jawk
+ * ჻჻჻჻჻჻
+ * Copyright (C) 2006 - 2026 MetricsHub
+ * ჻჻჻჻჻჻
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Lesser Public License for more details.
+ *
+ * You should have received a copy of the GNU General Lesser Public
+ * License along with this program. If not, see
+ * .
+ * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
+ */
+
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import io.jawk.intermediate.Opcode;
+
+/**
+ * Snapshot of tuple and function execution statistics collected by an
+ * {@link AVM}.
+ */
+public final class ProfilingReport {
+
+ private final List tupleEntries;
+ private final List functionEntries;
+
+ /**
+ * Returns an empty profiling report.
+ *
+ * @return empty report
+ */
+ public static ProfilingReport empty() {
+ return new ProfilingReport(
+ Collections.emptyMap(),
+ Collections.emptyMap());
+ }
+
+ ProfilingReport(Map tupleStats, Map functionStats) {
+ this.tupleEntries = Collections.unmodifiableList(toTupleEntries(tupleStats));
+ this.functionEntries = Collections.unmodifiableList(toFunctionEntries(functionStats));
+ }
+
+ /**
+ * Returns tuple execution statistics sorted by descending total time.
+ *
+ * @return tuple execution entries
+ */
+ public List getTupleEntries() {
+ return tupleEntries;
+ }
+
+ /**
+ * Returns function execution statistics sorted by descending total time.
+ *
+ * @return function execution entries
+ */
+ public List getFunctionEntries() {
+ return functionEntries;
+ }
+
+ /**
+ * Prints this profiling report to the supplied stream.
+ *
+ * @param out destination stream
+ */
+ public void print(PrintStream out) {
+ out.println("Jawk profiling report");
+ out.println();
+ printSection(out, "Tuple execution", tupleEntries);
+ out.println();
+ printSection(out, "Function execution", functionEntries);
+ }
+
+ private static void printSection(PrintStream out, String title, List entries) {
+ out.println(title + ":");
+ if (entries.isEmpty()) {
+ out.println(" (none)");
+ return;
+ }
+ out.printf(Locale.ROOT, " %-32s %12s %14s %14s%n", "Name", "Count", "Time (ms)", "Avg (ns)");
+ for (Entry entry : entries) {
+ out
+ .printf(
+ Locale.ROOT,
+ " %-32s %12d %14.3f %14.0f%n",
+ entry.getName(),
+ entry.getCount(),
+ entry.getTotalNanos() / 1_000_000.0d,
+ entry.getAverageNanos());
+ }
+ }
+
+ private static List toTupleEntries(Map stats) {
+ List entries = new ArrayList(stats.size());
+ for (Map.Entry entry : stats.entrySet()) {
+ entries.add(new Entry(entry.getKey().name(), entry.getValue().count, entry.getValue().totalNanos));
+ }
+ sort(entries);
+ return entries;
+ }
+
+ private static List toFunctionEntries(Map stats) {
+ List entries = new ArrayList(stats.size());
+ for (Map.Entry entry : stats.entrySet()) {
+ entries.add(new Entry(entry.getKey(), entry.getValue().count, entry.getValue().totalNanos));
+ }
+ sort(entries);
+ return entries;
+ }
+
+ private static void sort(List entries) {
+ Collections
+ .sort(
+ entries,
+ Comparator
+ .comparingLong(Entry::getTotalNanos)
+ .reversed()
+ .thenComparing(Comparator.comparingLong(Entry::getCount).reversed())
+ .thenComparing(Entry::getName));
+ }
+
+ static final class Accumulator {
+ private long count;
+ private long totalNanos;
+
+ void add(long elapsedNanos) {
+ count++;
+ totalNanos += elapsedNanos;
+ }
+ }
+
+ /**
+ * One profiling table row.
+ */
+ public static final class Entry {
+ private final String name;
+ private final long count;
+ private final long totalNanos;
+
+ private Entry(String name, long count, long totalNanos) {
+ this.name = name;
+ this.count = count;
+ this.totalNanos = totalNanos;
+ }
+
+ /**
+ * Returns the tuple type or function name.
+ *
+ * @return entry name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the number of observed executions.
+ *
+ * @return execution count
+ */
+ public long getCount() {
+ return count;
+ }
+
+ /**
+ * Returns the total elapsed time in nanoseconds.
+ *
+ * @return total elapsed nanoseconds
+ */
+ public long getTotalNanos() {
+ return totalNanos;
+ }
+
+ /**
+ * Returns the average elapsed time in nanoseconds.
+ *
+ * @return average elapsed nanoseconds
+ */
+ public double getAverageNanos() {
+ return count == 0 ? 0.0d : (double) totalNanos / count;
+ }
+ }
+}
diff --git a/src/main/java/io/jawk/backend/SandboxedAVM.java b/src/main/java/io/jawk/backend/SandboxedAVM.java
index 8d8a34d..a55a27d 100644
--- a/src/main/java/io/jawk/backend/SandboxedAVM.java
+++ b/src/main/java/io/jawk/backend/SandboxedAVM.java
@@ -42,7 +42,22 @@ public class SandboxedAVM extends AVM {
*/
public SandboxedAVM(AwkSettings parameters,
Map extensionInstances) {
- super(parameters, extensionInstances);
+ this(parameters, extensionInstances, false);
+ }
+
+ /**
+ * Creates a sandboxed AVM with the provided settings, extension instances, and
+ * profiling mode.
+ *
+ * @param parameters Runtime settings to honor
+ * @param extensionInstances Available extension implementations
+ * @param profilingEnabled Whether to collect profiling statistics
+ */
+ public SandboxedAVM(
+ AwkSettings parameters,
+ Map extensionInstances,
+ boolean profilingEnabled) {
+ super(parameters, extensionInstances, profilingEnabled);
}
@Override
diff --git a/src/main/java/io/jawk/jrt/ListAssocArray.java b/src/main/java/io/jawk/jrt/ListAssocArray.java
index 486cc4a..0da3d93 100644
--- a/src/main/java/io/jawk/jrt/ListAssocArray.java
+++ b/src/main/java/io/jawk/jrt/ListAssocArray.java
@@ -4,7 +4,7 @@
* ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
* Jawk
* ჻჻჻჻჻჻
- * Copyright 2006 - 2026 MetricsHub
+ * Copyright (C) 2006 - 2026 MetricsHub
* ჻჻჻჻჻჻
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
diff --git a/src/site/markdown/cli-reference.md b/src/site/markdown/cli-reference.md
index 87ac157..0a3b3e2 100644
--- a/src/site/markdown/cli-reference.md
+++ b/src/site/markdown/cli-reference.md
@@ -63,6 +63,8 @@ java -jar jawk-${project.version}-standalone.jar --list-ext
> - `--dump-syntax` prints the parsed abstract syntax tree and skips execution.
> - `--dump-intermediate` prints the tuple stream and skips execution.
> - `-s` or `--no-optimize` disables tuple optimization during compilation.
+> - `--profile` executes the script with runtime profiling enabled and prints tuple and function timing statistics to stderr.
+> - `--profile=` writes the same profiling report to the specified file instead of stderr.
>
> - Help and errors
>
@@ -73,6 +75,8 @@ java -jar jawk-${project.version}-standalone.jar --list-ext
## Execution Notes
- `--dump-syntax`, `--dump-intermediate`, `-K`, `-h`, `-?`, and `--list-ext` are non-executing modes.
+- `--profile` is an executing mode. It keeps normal AWK output on stdout and writes the profiling report to stderr after execution finishes.
+- `--profile=` keeps normal AWK output on stdout and writes only the profiling report to the file.
- `-S` affects compilation and execution, not just runtime behavior.
- `--posix` currently disables arrays-of-arrays syntax and related subarray-only operands in order to keep CLI compilation aligned with classic POSIX-style AWK expectations.
- `--posix` is rejected together with `-L`, because loading precompiled tuples bypasses source compilation entirely.
diff --git a/src/test/java/io/jawk/AwkTestSupport.java b/src/test/java/io/jawk/AwkTestSupport.java
index 839729a..70aaa91 100644
--- a/src/test/java/io/jawk/AwkTestSupport.java
+++ b/src/test/java/io/jawk/AwkTestSupport.java
@@ -168,6 +168,7 @@ default void runAndAssert() throws Exception {
public static final class TestResult {
private final String description;
private final String output;
+ private final String errorOutput;
private final int exitCode;
private final String expectedOutput;
private final List expectedLines;
@@ -178,6 +179,7 @@ public static final class TestResult {
TestResult(
String description,
String output,
+ String errorOutput,
int exitCode,
String expectedOutput,
List expectedLines,
@@ -186,6 +188,7 @@ public static final class TestResult {
Throwable thrownException) {
this.description = description;
this.output = output;
+ this.errorOutput = errorOutput;
this.exitCode = exitCode;
this.expectedOutput = expectedOutput;
this.expectedLines = expectedLines != null ? Collections.unmodifiableList(new ArrayList<>(expectedLines)) : null;
@@ -212,6 +215,15 @@ public String output() {
return output;
}
+ /**
+ * Returns the captured stderr of the test execution.
+ *
+ * @return the captured error output as a UTF-8 string
+ */
+ public String errorOutput() {
+ return errorOutput;
+ }
+
/**
* Returns the exit code reported by the execution.
*
@@ -935,6 +947,7 @@ private TestResult executeAndCapture(ExecutionEnvironment env) throws Exception
return new TestResult(
layout.description,
actualOutput,
+ result.errorOutput,
result.exitCode,
expected,
expectedLines,
@@ -946,6 +959,7 @@ private TestResult executeAndCapture(ExecutionEnvironment env) throws Exception
return new TestResult(
layout.description,
"",
+ "",
0,
null,
null,
@@ -1069,7 +1083,7 @@ protected ActualResult execute(ExecutionEnvironment env) throws Exception {
} catch (ExitException ex) {
exitCode = ex.getCode();
}
- return new ActualResult(out.toString(), exitCode);
+ return new ActualResult(out.toString(), "", exitCode);
}
}
@@ -1134,6 +1148,7 @@ protected ActualResult execute(ExecutionEnvironment env) throws Exception {
}
return new ActualResult(
outBytes.toString(StandardCharsets.UTF_8.name()),
+ errBytes.toString(StandardCharsets.UTF_8.name()),
exitCode);
}
}
@@ -1176,10 +1191,12 @@ private String replacePlaceholders(String value, boolean escapeForScript) {
private static final class ActualResult {
final String output;
+ final String errorOutput;
final int exitCode;
- ActualResult(String output, int exitCode) {
+ ActualResult(String output, String errorOutput, int exitCode) {
this.output = output;
+ this.errorOutput = errorOutput;
this.exitCode = exitCode;
}
}
diff --git a/src/test/java/io/jawk/CliOptionTest.java b/src/test/java/io/jawk/CliOptionTest.java
index 109caa8..98a5a09 100644
--- a/src/test/java/io/jawk/CliOptionTest.java
+++ b/src/test/java/io/jawk/CliOptionTest.java
@@ -35,7 +35,9 @@
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;
+import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@@ -65,6 +67,89 @@ public void longNoOptimizeOptionDisablesOptimization() {
assertTrue(cli.isDisableOptimize());
}
+ @Test
+ public void profileOptionPrintsReportToStderr() throws Exception {
+ AwkTestSupport.TestResult result = AwkTestSupport
+ .cliTest("CLI --profile prints runtime report")
+ .argument("--profile")
+ .script("function inc(x) { return x + 1 } BEGIN { print inc(1); print inc(2) }")
+ .expect("2\n3\n")
+ .run();
+
+ result.assertExpected();
+ assertTrue(result.errorOutput().contains("Jawk profiling report"));
+ assertTrue(result.errorOutput().contains("Tuple execution:"));
+ assertTrue(result.errorOutput().contains("Function execution:"));
+ assertTrue(result.errorOutput().contains("CALL_FUNCTION"));
+ assertTrue(result.errorOutput().contains("inc"));
+ }
+
+ @Test
+ public void profileOptionRecordsFunctionThatExitsWithoutReturning() throws Exception {
+ AwkTestSupport.TestResult result = AwkTestSupport
+ .cliTest("CLI --profile records function exit")
+ .argument("--profile")
+ .script("function stop() { exit } END { stop() }")
+ .expect("")
+ .expectExit(0)
+ .run();
+
+ result.assertExpected();
+ assertTrue(result.errorOutput().contains("Jawk profiling report"));
+ assertTrue(result.errorOutput().contains("stop"));
+ }
+
+ @Test
+ public void profileOptionWithFilenameWritesReportToFile() throws Exception {
+ File profile = tempFolder.newFile("profile.txt");
+ assertTrue(profile.delete());
+
+ AwkTestSupport.TestResult result = AwkTestSupport
+ .cliTest("CLI --profile=file writes runtime report")
+ .argument("--profile=" + profile.getAbsolutePath())
+ .script("function inc(x) { return x + 1 } BEGIN { print inc(1) }")
+ .expect("2\n")
+ .run();
+
+ result.assertExpected();
+ assertEquals("", result.errorOutput());
+ assertTrue(profile.isFile());
+ String report = new String(Files.readAllBytes(profile.toPath()), StandardCharsets.UTF_8);
+ assertTrue(report.contains("Jawk profiling report"));
+ assertTrue(report.contains("Tuple execution:"));
+ assertTrue(report.contains("Function execution:"));
+ assertTrue(report.contains("CALL_FUNCTION"));
+ assertTrue(report.contains("inc"));
+ }
+
+ @Test
+ public void profileOptionWriteFailureIsReportedAsIoFailure() throws Exception {
+ File profileDirectory = tempFolder.newFolder("profile-directory");
+
+ AwkTestSupport.TestResult result = AwkTestSupport
+ .cliTest("CLI --profile=file reports write failure")
+ .argument("--profile=" + profileDirectory.getAbsolutePath())
+ .script("BEGIN { print 1 }")
+ .expectThrow(UncheckedIOException.class)
+ .run();
+
+ result.assertExpected();
+ assertTrue(result.thrownException().getMessage().contains("Failed to write profiling report"));
+ }
+
+ @Test
+ public void profileOptionWithEmptyFilenameIsRejected() throws Exception {
+ AwkTestSupport.TestResult result = AwkTestSupport
+ .cliTest("CLI --profile= rejects empty filename")
+ .argument("--profile=")
+ .script("BEGIN { print 1 }")
+ .expectThrow(IllegalArgumentException.class)
+ .run();
+
+ result.assertExpected();
+ assertTrue(result.thrownException().getMessage().contains("Need output filename for --profile"));
+ }
+
@Test
public void posixOptionDisablesArraysOfArrays() {
Cli cli = new Cli();