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();