From b16098057f14b9e6f5e43636e87141852aa9cdc3 Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Wed, 6 May 2026 20:45:50 +0200 Subject: [PATCH 1/6] Add profiling report file support --- .../site/CompatibilitySummaryGenerator.java | 2 +- src/main/java/io/jawk/Cli.java | 57 +++++- src/main/java/io/jawk/ProfilingAwk.java | 121 ++++++++++++ src/main/java/io/jawk/backend/AVM.java | 29 ++- .../java/io/jawk/backend/ProfilingAVM.java | 132 +++++++++++++ .../java/io/jawk/backend/ProfilingReport.java | 187 ++++++++++++++++++ .../jawk/backend/ProfilingSandboxedAVM.java | 54 +++++ src/main/java/io/jawk/jrt/ListAssocArray.java | 2 +- src/site/markdown/cli-reference.md | 4 + src/test/java/io/jawk/AwkTestSupport.java | 21 +- src/test/java/io/jawk/CliOptionTest.java | 54 +++++ 11 files changed, 657 insertions(+), 6 deletions(-) create mode 100644 src/main/java/io/jawk/ProfilingAwk.java create mode 100644 src/main/java/io/jawk/backend/ProfilingAVM.java create mode 100644 src/main/java/io/jawk/backend/ProfilingReport.java create mode 100644 src/main/java/io/jawk/backend/ProfilingSandboxedAVM.java 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/Cli.java b/src/main/java/io/jawk/Cli.java index 0da3c2e..94e5ba3 100644 --- a/src/main/java/io/jawk/Cli.java +++ b/src/main/java/io/jawk/Cli.java @@ -24,6 +24,7 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.jawk.backend.AVM; +import io.jawk.backend.ProfilingAVM; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; @@ -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; @@ -406,8 +429,12 @@ public void run() throws Exception { } Awk awk; - if (sandbox) { + if (sandbox && profiling) { + awk = new ProfilingSandboxedAwk(extensions, settings); + } else if (sandbox) { awk = extensions.isEmpty() ? new SandboxedAwk(settings) : new SandboxedAwk(extensions, settings); + } else if (profiling) { + awk = extensions.isEmpty() ? new ProfilingAwk(settings) : new ProfilingAwk(extensions, settings); } else { awk = extensions.isEmpty() ? new Awk(settings) : new Awk(extensions, settings); } @@ -493,10 +520,34 @@ private void executeProgram(Awk awk, AwkProgram program, File memoryFile) throws if (memoryFile != null) { savePersistentMemory(avm, memoryFile); } + printProfilingReport(avm); } } } + private void printProfilingReport(AVM avm) { + if (!profiling || !(avm instanceof ProfilingAVM)) { + return; + } + if (profilingOutputFile == null) { + err.println(); + ((ProfilingAVM) avm).getProfilingReport().print(err); + return; + } + File parent = profilingOutputFile.getAbsoluteFile().getParentFile(); + if (parent != null && !parent.isDirectory() && !parent.mkdirs() && !parent.isDirectory()) { + throw new IllegalArgumentException( + "Failed to create directory '" + parent + "' for profiling report."); + } + try (PrintStream profileOut = new PrintStream(profilingOutputFile, "UTF-8")) { + ((ProfilingAVM) avm).getProfilingReport().print(profileOut); + } catch (IOException ex) { + throw new IllegalArgumentException( + "Failed to write profiling report '" + profilingOutputFile + "': " + ex.getMessage(), + ex); + } + } + /** * Restores persistent user-defined globals from the specified file when it * already exists. @@ -571,6 +622,7 @@ private static void usage(PrintStream dest) { " [--dump-syntax]" + " [--dump-intermediate]" + " [-s|--no-optimize]" + + " [--profile[=filename]]" + " [--locale locale]" + " [-t]" + " [-l extension]..." + @@ -605,6 +657,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(); diff --git a/src/main/java/io/jawk/ProfilingAwk.java b/src/main/java/io/jawk/ProfilingAwk.java new file mode 100644 index 0000000..4403d81 --- /dev/null +++ b/src/main/java/io/jawk/ProfilingAwk.java @@ -0,0 +1,121 @@ +package io.jawk; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * Jawk + * ჻჻჻჻჻჻ + * Copyright 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.util.Collection; +import java.util.List; +import io.jawk.backend.AVM; +import io.jawk.backend.ProfilingAVM; +import io.jawk.backend.ProfilingSandboxedAVM; +import io.jawk.ext.JawkExtension; +import io.jawk.util.AwkSettings; +import io.jawk.util.ScriptSource; + +/** + * {@link Awk} variant that creates {@link ProfilingAVM} runtimes. + */ +public final class ProfilingAwk extends Awk { + + /** + * Creates a profiling AWK instance with default settings and no extensions. + */ + public ProfilingAwk() { + super(); + } + + /** + * Creates a profiling AWK instance with the specified settings. + * + * @param settings behavioral configuration for this engine + */ + public ProfilingAwk(AwkSettings settings) { + super(settings); + } + + /** + * Creates a profiling AWK instance with the specified extension instances. + * + * @param extensions extension instances implementing {@link JawkExtension} + */ + public ProfilingAwk(Collection extensions) { + super(extensions); + } + + /** + * Creates a profiling AWK instance with the specified extension instances and + * settings. + * + * @param extensions extension instances implementing {@link JawkExtension} + * @param settings behavioral configuration for this engine + */ + public ProfilingAwk(Collection extensions, AwkSettings settings) { + super(extensions, settings); + } + + /** + * Creates a profiling AWK instance with the supplied extensions. + * + * @param extensions extension instances implementing {@link JawkExtension} + */ + @SafeVarargs + public ProfilingAwk(JawkExtension... extensions) { + super(extensions); + } + + @Override + public AVM createAvm() { + return createAvm(getSettings()); + } + + @Override + protected AVM createAvm(AwkSettings settingsParam) { + return new ProfilingAVM(settingsParam, getExtensionInstances()); + } +} + +final class ProfilingSandboxedAwk extends Awk { + + ProfilingSandboxedAwk(Collection extensions, AwkSettings settings) { + super(extensions, settings); + } + + @Override + public AwkProgram compile(List scripts, boolean disableOptimizeParam) throws java.io.IOException { + return compileProgram(scripts, disableOptimizeParam, new SandboxedCompiledAwkProgram()); + } + + @Override + public AwkExpression compileExpression(String expression, boolean disableOptimizeParam) throws java.io.IOException { + return compileExpression(expression, disableOptimizeParam, new SandboxedCompiledAwkExpression()); + } + + @Override + public AVM createAvm() { + return createAvm(getSettings()); + } + + @Override + protected AVM createAvm(AwkSettings settingsParam) { + return new ProfilingSandboxedAVM(settingsParam, getExtensionInstances()); + } +} diff --git a/src/main/java/io/jawk/backend/AVM.java b/src/main/java/io/jawk/backend/AVM.java index ae2a20f..4e4eb89 100644 --- a/src/main/java/io/jawk/backend/AVM.java +++ b/src/main/java/io/jawk/backend/AVM.java @@ -979,6 +979,7 @@ private void executeTuples(PositionTracker position) while (!position.isEOF()) { // System_out.println("--> "+position); opcode = position.opcode(); + long tupleStartNanos = beforeTupleExecution(position, opcode); // switch on OPCODE switch (opcode) { case PRINT: { @@ -2217,7 +2218,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,6 +2813,7 @@ private void executeTuples(PositionTracker position) default: throw new Error("invalid opcode: " + position.opcode()); } + afterTupleExecution(position, opcode, tupleStartNanos); } } catch (IOException ioe) { @@ -2842,6 +2845,30 @@ private void executeTuples(PositionTracker position) } } + /** + * Hook invoked immediately before a tuple is executed. + * + * @param position current tuple position + * @param opcode opcode about to execute + * @return hook-specific state passed to + * {@link #afterTupleExecution(PositionTracker, Opcode, long)} + */ + protected long beforeTupleExecution(PositionTracker position, Opcode opcode) { + return 0L; + } + + /** + * Hook invoked immediately after a tuple execution attempt completes. + * + * @param position current tuple position after execution + * @param opcode opcode that was executed + * @param tupleStartNanos state returned by + * {@link #beforeTupleExecution(PositionTracker, Opcode)} + */ + protected void afterTupleExecution(PositionTracker position, Opcode opcode, long tupleStartNanos) { + // default AVM execution does not collect profiling information + } + /** * Releases any prepared input source and runtime I/O resources owned by this * AVM. diff --git a/src/main/java/io/jawk/backend/ProfilingAVM.java b/src/main/java/io/jawk/backend/ProfilingAVM.java new file mode 100644 index 0000000..fb47a0d --- /dev/null +++ b/src/main/java/io/jawk/backend/ProfilingAVM.java @@ -0,0 +1,132 @@ +package io.jawk.backend; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * Jawk + * ჻჻჻჻჻჻ + * Copyright 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.util.ArrayDeque; +import java.util.Collections; +import java.util.Deque; +import java.util.EnumMap; +import java.util.LinkedHashMap; +import java.util.Map; +import io.jawk.ext.ExtensionFunction; +import io.jawk.ext.JawkExtension; +import io.jawk.intermediate.Opcode; +import io.jawk.intermediate.PositionTracker; +import io.jawk.util.AwkSettings; + +/** + * {@link AVM} variant that collects tuple and function execution statistics. + */ +public class ProfilingAVM extends AVM { + + private final Map tupleStats = new EnumMap( + Opcode.class); + private final Map functionStats = new LinkedHashMap(); + private final Deque activeFunctions = new ArrayDeque(); + + /** + * Creates a profiling AVM with default settings and no extensions. + */ + public ProfilingAVM() { + this(null, Collections.emptyMap()); + } + + /** + * Creates a profiling AVM with the provided settings and extension instances. + * + * @param parameters Runtime settings to honor + * @param extensionInstances Available extension implementations + */ + public ProfilingAVM(AwkSettings parameters, + Map extensionInstances) { + super(parameters, extensionInstances); + } + + @Override + protected long beforeTupleExecution(PositionTracker position, Opcode opcode) { + long now = System.nanoTime(); + if (opcode == Opcode.CALL_FUNCTION) { + activeFunctions.push(new ActiveFunction(position.stringArg(1), now)); + } else if (opcode == Opcode.EXTENSION) { + ExtensionFunction function = position.extensionFunctionArg(); + activeFunctions.push(new ActiveFunction(function.getKeyword(), now)); + } + return now; + } + + @Override + protected void afterTupleExecution(PositionTracker position, Opcode opcode, long tupleStartNanos) { + long now = System.nanoTime(); + statisticsFor(tupleStats, opcode).add(now - tupleStartNanos); + if (opcode == Opcode.EXTENSION || opcode == Opcode.RETURN_FROM_FUNCTION) { + recordFunctionExit(now); + } + } + + /** + * Clears all collected profiling statistics. + */ + public void resetProfiling() { + tupleStats.clear(); + functionStats.clear(); + activeFunctions.clear(); + } + + /** + * Returns an immutable snapshot of the collected profiling statistics. + * + * @return profiling report snapshot + */ + public ProfilingReport getProfilingReport() { + return new ProfilingReport(tupleStats, functionStats); + } + + 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 (activeFunctions.isEmpty()) { + return; + } + ActiveFunction function = activeFunctions.pop(); + statisticsFor(functionStats, function.name).add(now - function.startNanos); + } + + 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; + } + } +} 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..4c44379 --- /dev/null +++ b/src/main/java/io/jawk/backend/ProfilingReport.java @@ -0,0 +1,187 @@ +package io.jawk.backend; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * Jawk + * ჻჻჻჻჻჻ + * Copyright 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 a + * {@link ProfilingAVM}. + */ +public final class ProfilingReport { + + private final List tupleEntries; + private final List functionEntries; + + 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/ProfilingSandboxedAVM.java b/src/main/java/io/jawk/backend/ProfilingSandboxedAVM.java new file mode 100644 index 0000000..ea943be --- /dev/null +++ b/src/main/java/io/jawk/backend/ProfilingSandboxedAVM.java @@ -0,0 +1,54 @@ +package io.jawk.backend; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * Jawk + * ჻჻჻჻჻჻ + * Copyright 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.util.Map; +import io.jawk.ext.JawkExtension; +import io.jawk.jrt.AwkSink; +import io.jawk.jrt.JRT; +import io.jawk.jrt.SandboxedJRT; +import io.jawk.util.AwkSettings; + +/** + * Profiling AVM variant enforcing sandbox restrictions at runtime. + */ +public class ProfilingSandboxedAVM extends ProfilingAVM { + + /** + * Creates a profiling sandboxed AVM with the provided settings and extension + * instances. + * + * @param parameters Runtime settings to honor + * @param extensionInstances Available extension implementations + */ + public ProfilingSandboxedAVM(AwkSettings parameters, + Map extensionInstances) { + super(parameters, extensionInstances); + } + + @Override + protected JRT createJrt() { + AwkSettings s = getSettings(); + return new SandboxedJRT(this, s.getLocale(), AwkSink.NOP_SINK, null); + } +} 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..59287e2 100644 --- a/src/test/java/io/jawk/CliOptionTest.java +++ b/src/test/java/io/jawk/CliOptionTest.java @@ -36,6 +36,7 @@ import java.io.OutputStream; import java.io.PrintStream; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -65,6 +66,59 @@ 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 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 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(); From 188e6eb665b7e7ee5b3a86ab2d80b639da6998dc Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Wed, 6 May 2026 21:39:45 +0200 Subject: [PATCH 2/6] Simplify profiling runtime and CLI handling --- src/main/java/io/jawk/Awk.java | 25 +++- src/main/java/io/jawk/Cli.java | 20 +-- src/main/java/io/jawk/ProfilingAwk.java | 121 ---------------- src/main/java/io/jawk/SandboxedAwk.java | 12 +- src/main/java/io/jawk/backend/AVM.java | 124 +++++++++++++--- .../java/io/jawk/backend/ProfilingAVM.java | 132 ------------------ .../java/io/jawk/backend/ProfilingReport.java | 17 ++- .../jawk/backend/ProfilingSandboxedAVM.java | 54 ------- .../java/io/jawk/backend/SandboxedAVM.java | 17 ++- src/test/java/io/jawk/CliOptionTest.java | 15 ++ 10 files changed, 194 insertions(+), 343 deletions(-) delete mode 100644 src/main/java/io/jawk/ProfilingAwk.java delete mode 100644 src/main/java/io/jawk/backend/ProfilingAVM.java delete mode 100644 src/main/java/io/jawk/backend/ProfilingSandboxedAVM.java 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 94e5ba3..b32716e 100644 --- a/src/main/java/io/jawk/Cli.java +++ b/src/main/java/io/jawk/Cli.java @@ -24,7 +24,6 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.jawk.backend.AVM; -import io.jawk.backend.ProfilingAVM; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; @@ -429,12 +428,8 @@ public void run() throws Exception { } Awk awk; - if (sandbox && profiling) { - awk = new ProfilingSandboxedAwk(extensions, settings); - } else if (sandbox) { + if (sandbox) { awk = extensions.isEmpty() ? new SandboxedAwk(settings) : new SandboxedAwk(extensions, settings); - } else if (profiling) { - awk = extensions.isEmpty() ? new ProfilingAwk(settings) : new ProfilingAwk(extensions, settings); } else { awk = extensions.isEmpty() ? new Awk(settings) : new Awk(extensions, settings); } @@ -497,7 +492,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) { @@ -520,18 +515,17 @@ private void executeProgram(Awk awk, AwkProgram program, File memoryFile) throws if (memoryFile != null) { savePersistentMemory(avm, memoryFile); } - printProfilingReport(avm); + if (profiling) { + printProfilingReport(avm); + } } } } private void printProfilingReport(AVM avm) { - if (!profiling || !(avm instanceof ProfilingAVM)) { - return; - } if (profilingOutputFile == null) { err.println(); - ((ProfilingAVM) avm).getProfilingReport().print(err); + avm.getProfilingReport().print(err); return; } File parent = profilingOutputFile.getAbsoluteFile().getParentFile(); @@ -540,7 +534,7 @@ private void printProfilingReport(AVM avm) { "Failed to create directory '" + parent + "' for profiling report."); } try (PrintStream profileOut = new PrintStream(profilingOutputFile, "UTF-8")) { - ((ProfilingAVM) avm).getProfilingReport().print(profileOut); + avm.getProfilingReport().print(profileOut); } catch (IOException ex) { throw new IllegalArgumentException( "Failed to write profiling report '" + profilingOutputFile + "': " + ex.getMessage(), diff --git a/src/main/java/io/jawk/ProfilingAwk.java b/src/main/java/io/jawk/ProfilingAwk.java deleted file mode 100644 index 4403d81..0000000 --- a/src/main/java/io/jawk/ProfilingAwk.java +++ /dev/null @@ -1,121 +0,0 @@ -package io.jawk; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * Jawk - * ჻჻჻჻჻჻ - * Copyright 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.util.Collection; -import java.util.List; -import io.jawk.backend.AVM; -import io.jawk.backend.ProfilingAVM; -import io.jawk.backend.ProfilingSandboxedAVM; -import io.jawk.ext.JawkExtension; -import io.jawk.util.AwkSettings; -import io.jawk.util.ScriptSource; - -/** - * {@link Awk} variant that creates {@link ProfilingAVM} runtimes. - */ -public final class ProfilingAwk extends Awk { - - /** - * Creates a profiling AWK instance with default settings and no extensions. - */ - public ProfilingAwk() { - super(); - } - - /** - * Creates a profiling AWK instance with the specified settings. - * - * @param settings behavioral configuration for this engine - */ - public ProfilingAwk(AwkSettings settings) { - super(settings); - } - - /** - * Creates a profiling AWK instance with the specified extension instances. - * - * @param extensions extension instances implementing {@link JawkExtension} - */ - public ProfilingAwk(Collection extensions) { - super(extensions); - } - - /** - * Creates a profiling AWK instance with the specified extension instances and - * settings. - * - * @param extensions extension instances implementing {@link JawkExtension} - * @param settings behavioral configuration for this engine - */ - public ProfilingAwk(Collection extensions, AwkSettings settings) { - super(extensions, settings); - } - - /** - * Creates a profiling AWK instance with the supplied extensions. - * - * @param extensions extension instances implementing {@link JawkExtension} - */ - @SafeVarargs - public ProfilingAwk(JawkExtension... extensions) { - super(extensions); - } - - @Override - public AVM createAvm() { - return createAvm(getSettings()); - } - - @Override - protected AVM createAvm(AwkSettings settingsParam) { - return new ProfilingAVM(settingsParam, getExtensionInstances()); - } -} - -final class ProfilingSandboxedAwk extends Awk { - - ProfilingSandboxedAwk(Collection extensions, AwkSettings settings) { - super(extensions, settings); - } - - @Override - public AwkProgram compile(List scripts, boolean disableOptimizeParam) throws java.io.IOException { - return compileProgram(scripts, disableOptimizeParam, new SandboxedCompiledAwkProgram()); - } - - @Override - public AwkExpression compileExpression(String expression, boolean disableOptimizeParam) throws java.io.IOException { - return compileExpression(expression, disableOptimizeParam, new SandboxedCompiledAwkExpression()); - } - - @Override - public AVM createAvm() { - return createAvm(getSettings()); - } - - @Override - protected AVM createAvm(AwkSettings settingsParam) { - return new ProfilingSandboxedAVM(settingsParam, getExtensionInstances()); - } -} 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 4e4eb89..e313be9 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,11 +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(); - long tupleStartNanos = beforeTupleExecution(position, opcode); + if (profiling) { + tupleStartNanos = beforeProfiledTuple(position, opcode); + } // switch on OPCODE switch (opcode) { case PRINT: { @@ -2813,7 +2845,9 @@ private void executeTuples(PositionTracker position) default: throw new Error("invalid opcode: " + position.opcode()); } - afterTupleExecution(position, opcode, tupleStartNanos); + if (profiling) { + afterProfiledTuple(opcode, tupleStartNanos); + } } } catch (IOException ioe) { @@ -2846,27 +2880,83 @@ private void executeTuples(PositionTracker position) } /** - * Hook invoked immediately before a tuple is executed. - * - * @param position current tuple position - * @param opcode opcode about to execute - * @return hook-specific state passed to - * {@link #afterTupleExecution(PositionTracker, Opcode, long)} + * Clears all collected profiling statistics. */ - protected long beforeTupleExecution(PositionTracker position, Opcode opcode) { - return 0L; + public void resetProfiling() { + if (!profiling) { + return; + } + tupleProfilingStats.clear(); + functionProfilingStats.clear(); + activeProfilingFunctions.clear(); } /** - * Hook invoked immediately after a tuple execution attempt completes. + * Returns an immutable snapshot of the collected profiling statistics. * - * @param position current tuple position after execution - * @param opcode opcode that was executed - * @param tupleStartNanos state returned by - * {@link #beforeTupleExecution(PositionTracker, Opcode)} + * @return profiling report snapshot */ - protected void afterTupleExecution(PositionTracker position, Opcode opcode, long tupleStartNanos) { - // default AVM execution does not collect profiling information + 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; + } } /** diff --git a/src/main/java/io/jawk/backend/ProfilingAVM.java b/src/main/java/io/jawk/backend/ProfilingAVM.java deleted file mode 100644 index fb47a0d..0000000 --- a/src/main/java/io/jawk/backend/ProfilingAVM.java +++ /dev/null @@ -1,132 +0,0 @@ -package io.jawk.backend; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * Jawk - * ჻჻჻჻჻჻ - * Copyright 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.util.ArrayDeque; -import java.util.Collections; -import java.util.Deque; -import java.util.EnumMap; -import java.util.LinkedHashMap; -import java.util.Map; -import io.jawk.ext.ExtensionFunction; -import io.jawk.ext.JawkExtension; -import io.jawk.intermediate.Opcode; -import io.jawk.intermediate.PositionTracker; -import io.jawk.util.AwkSettings; - -/** - * {@link AVM} variant that collects tuple and function execution statistics. - */ -public class ProfilingAVM extends AVM { - - private final Map tupleStats = new EnumMap( - Opcode.class); - private final Map functionStats = new LinkedHashMap(); - private final Deque activeFunctions = new ArrayDeque(); - - /** - * Creates a profiling AVM with default settings and no extensions. - */ - public ProfilingAVM() { - this(null, Collections.emptyMap()); - } - - /** - * Creates a profiling AVM with the provided settings and extension instances. - * - * @param parameters Runtime settings to honor - * @param extensionInstances Available extension implementations - */ - public ProfilingAVM(AwkSettings parameters, - Map extensionInstances) { - super(parameters, extensionInstances); - } - - @Override - protected long beforeTupleExecution(PositionTracker position, Opcode opcode) { - long now = System.nanoTime(); - if (opcode == Opcode.CALL_FUNCTION) { - activeFunctions.push(new ActiveFunction(position.stringArg(1), now)); - } else if (opcode == Opcode.EXTENSION) { - ExtensionFunction function = position.extensionFunctionArg(); - activeFunctions.push(new ActiveFunction(function.getKeyword(), now)); - } - return now; - } - - @Override - protected void afterTupleExecution(PositionTracker position, Opcode opcode, long tupleStartNanos) { - long now = System.nanoTime(); - statisticsFor(tupleStats, opcode).add(now - tupleStartNanos); - if (opcode == Opcode.EXTENSION || opcode == Opcode.RETURN_FROM_FUNCTION) { - recordFunctionExit(now); - } - } - - /** - * Clears all collected profiling statistics. - */ - public void resetProfiling() { - tupleStats.clear(); - functionStats.clear(); - activeFunctions.clear(); - } - - /** - * Returns an immutable snapshot of the collected profiling statistics. - * - * @return profiling report snapshot - */ - public ProfilingReport getProfilingReport() { - return new ProfilingReport(tupleStats, functionStats); - } - - 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 (activeFunctions.isEmpty()) { - return; - } - ActiveFunction function = activeFunctions.pop(); - statisticsFor(functionStats, function.name).add(now - function.startNanos); - } - - 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; - } - } -} diff --git a/src/main/java/io/jawk/backend/ProfilingReport.java b/src/main/java/io/jawk/backend/ProfilingReport.java index 4c44379..8dd2bb7 100644 --- a/src/main/java/io/jawk/backend/ProfilingReport.java +++ b/src/main/java/io/jawk/backend/ProfilingReport.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 @@ -32,14 +32,25 @@ import io.jawk.intermediate.Opcode; /** - * Snapshot of tuple and function execution statistics collected by a - * {@link ProfilingAVM}. + * 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)); diff --git a/src/main/java/io/jawk/backend/ProfilingSandboxedAVM.java b/src/main/java/io/jawk/backend/ProfilingSandboxedAVM.java deleted file mode 100644 index ea943be..0000000 --- a/src/main/java/io/jawk/backend/ProfilingSandboxedAVM.java +++ /dev/null @@ -1,54 +0,0 @@ -package io.jawk.backend; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * Jawk - * ჻჻჻჻჻჻ - * Copyright 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.util.Map; -import io.jawk.ext.JawkExtension; -import io.jawk.jrt.AwkSink; -import io.jawk.jrt.JRT; -import io.jawk.jrt.SandboxedJRT; -import io.jawk.util.AwkSettings; - -/** - * Profiling AVM variant enforcing sandbox restrictions at runtime. - */ -public class ProfilingSandboxedAVM extends ProfilingAVM { - - /** - * Creates a profiling sandboxed AVM with the provided settings and extension - * instances. - * - * @param parameters Runtime settings to honor - * @param extensionInstances Available extension implementations - */ - public ProfilingSandboxedAVM(AwkSettings parameters, - Map extensionInstances) { - super(parameters, extensionInstances); - } - - @Override - protected JRT createJrt() { - AwkSettings s = getSettings(); - return new SandboxedJRT(this, s.getLocale(), AwkSink.NOP_SINK, null); - } -} 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/test/java/io/jawk/CliOptionTest.java b/src/test/java/io/jawk/CliOptionTest.java index 59287e2..9730af0 100644 --- a/src/test/java/io/jawk/CliOptionTest.java +++ b/src/test/java/io/jawk/CliOptionTest.java @@ -83,6 +83,21 @@ public void profileOptionPrintsReportToStderr() throws Exception { 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 } BEGIN { 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"); From 8feef45385801ca23583ccc01a2b6c7502b516fb Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Thu, 7 May 2026 07:57:31 +0200 Subject: [PATCH 3/6] Handle profiling on exit exceptions --- src/main/java/io/jawk/backend/AVM.java | 5 +++++ src/test/java/io/jawk/CliOptionTest.java | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/jawk/backend/AVM.java b/src/main/java/io/jawk/backend/AVM.java index e313be9..d84a815 100644 --- a/src/main/java/io/jawk/backend/AVM.java +++ b/src/main/java/io/jawk/backend/AVM.java @@ -2850,6 +2850,11 @@ private void executeTuples(PositionTracker position) } } + } 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(); diff --git a/src/test/java/io/jawk/CliOptionTest.java b/src/test/java/io/jawk/CliOptionTest.java index 9730af0..36e047b 100644 --- a/src/test/java/io/jawk/CliOptionTest.java +++ b/src/test/java/io/jawk/CliOptionTest.java @@ -88,7 +88,7 @@ public void profileOptionRecordsFunctionThatExitsWithoutReturning() throws Excep AwkTestSupport.TestResult result = AwkTestSupport .cliTest("CLI --profile records function exit") .argument("--profile") - .script("function stop() { exit } BEGIN { stop() }") + .script("function stop() { exit } END { stop() }") .expect("") .expectExit(0) .run(); From 49ce2bf9c1f8a8530010b171293aa97f3091cead Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Thu, 7 May 2026 08:14:09 +0200 Subject: [PATCH 4/6] Surface profiling write failures as UncheckedIOException --- src/main/java/io/jawk/Cli.java | 11 ++++++++--- src/test/java/io/jawk/CliOptionTest.java | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/jawk/Cli.java b/src/main/java/io/jawk/Cli.java index b32716e..56ef2fa 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; @@ -530,13 +531,14 @@ private void printProfilingReport(AVM avm) { } File parent = profilingOutputFile.getAbsoluteFile().getParentFile(); if (parent != null && !parent.isDirectory() && !parent.mkdirs() && !parent.isDirectory()) { - throw new IllegalArgumentException( - "Failed to create directory '" + parent + "' for profiling report."); + throw new UncheckedIOException( + "Failed to create directory '" + parent + "' for profiling report.", + new IOException(parent.toString())); } try (PrintStream profileOut = new PrintStream(profilingOutputFile, "UTF-8")) { avm.getProfilingReport().print(profileOut); } catch (IOException ex) { - throw new IllegalArgumentException( + throw new UncheckedIOException( "Failed to write profiling report '" + profilingOutputFile + "': " + ex.getMessage(), ex); } @@ -714,6 +716,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/test/java/io/jawk/CliOptionTest.java b/src/test/java/io/jawk/CliOptionTest.java index 36e047b..7c1d0cf 100644 --- a/src/test/java/io/jawk/CliOptionTest.java +++ b/src/test/java/io/jawk/CliOptionTest.java @@ -35,6 +35,7 @@ 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; @@ -121,6 +122,22 @@ public void profileOptionWithFilenameWritesReportToFile() throws Exception { 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 }") + .expect("1\n") + .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 From 176c94d5a3fd9da21015ff72c238ed3d356848df Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Thu, 7 May 2026 08:32:39 +0200 Subject: [PATCH 5/6] Handle profile report stream write errors --- src/main/java/io/jawk/Cli.java | 6 ++++++ src/test/java/io/jawk/CliOptionTest.java | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/jawk/Cli.java b/src/main/java/io/jawk/Cli.java index 56ef2fa..083e68f 100644 --- a/src/main/java/io/jawk/Cli.java +++ b/src/main/java/io/jawk/Cli.java @@ -537,6 +537,12 @@ private void printProfilingReport(AVM avm) { } 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(), diff --git a/src/test/java/io/jawk/CliOptionTest.java b/src/test/java/io/jawk/CliOptionTest.java index 7c1d0cf..98a5a09 100644 --- a/src/test/java/io/jawk/CliOptionTest.java +++ b/src/test/java/io/jawk/CliOptionTest.java @@ -130,7 +130,6 @@ public void profileOptionWriteFailureIsReportedAsIoFailure() throws Exception { .cliTest("CLI --profile=file reports write failure") .argument("--profile=" + profileDirectory.getAbsolutePath()) .script("BEGIN { print 1 }") - .expect("1\n") .expectThrow(UncheckedIOException.class) .run(); From 0f4c224244ab822e6c57e11db8854879c0b04f7e Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Thu, 7 May 2026 09:35:36 +0200 Subject: [PATCH 6/6] Refine profiling report directory error handling --- src/main/java/io/jawk/Cli.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/jawk/Cli.java b/src/main/java/io/jawk/Cli.java index 083e68f..e186f88 100644 --- a/src/main/java/io/jawk/Cli.java +++ b/src/main/java/io/jawk/Cli.java @@ -531,9 +531,8 @@ private void printProfilingReport(AVM avm) { } File parent = profilingOutputFile.getAbsoluteFile().getParentFile(); if (parent != null && !parent.isDirectory() && !parent.mkdirs() && !parent.isDirectory()) { - throw new UncheckedIOException( - "Failed to create directory '" + parent + "' for profiling report.", - new IOException(parent.toString())); + 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);