Skip to content

Commit 80206b7

Browse files
authored
Merge pull request #471 from jawkio/443-add-profiling-option
Add profiling report file support
2 parents 285c56d + 0f4c224 commit 80206b7

11 files changed

Lines changed: 542 additions & 9 deletions

File tree

src/it/java/io/jawk/site/CompatibilitySummaryGenerator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
55
* Jawk
66
* ჻჻჻჻჻჻
7-
* Copyright 2006 - 2026 MetricsHub
7+
* Copyright (C) 2006 - 2026 MetricsHub
88
* ჻჻჻჻჻჻
99
* This program is free software: you can redistribute it and/or modify
1010
* it under the terms of the GNU Lesser General Public License as

src/main/java/io/jawk/Awk.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,17 @@ public AVM createAvm() {
310310
return createAvm(this.settings);
311311
}
312312

313+
/**
314+
* Creates a reusable runtime backed by one {@link AVM} instance, optionally
315+
* collecting runtime profiling statistics.
316+
*
317+
* @param profilingEnabled whether runtime profiling should be enabled
318+
* @return reusable AVM
319+
*/
320+
public AVM createAvm(boolean profilingEnabled) {
321+
return createAvm(this.settings, profilingEnabled);
322+
}
323+
313324
/**
314325
* Starts building a run request for a compiled AWK program.
315326
* <p>
@@ -680,7 +691,19 @@ public AVM prepareEval(InputSource source) throws IOException {
680691
* @return reusable AVM
681692
*/
682693
protected AVM createAvm(AwkSettings settingsParam) {
683-
return new AVM(settingsParam, this.extensionInstances);
694+
return createAvm(settingsParam, false);
695+
}
696+
697+
/**
698+
* Creates an {@link AVM} using the provided runtime settings and profiling
699+
* mode.
700+
*
701+
* @param settingsParam runtime settings to apply
702+
* @param profilingEnabled whether runtime profiling should be enabled
703+
* @return reusable AVM
704+
*/
705+
protected AVM createAvm(AwkSettings settingsParam, boolean profilingEnabled) {
706+
return new AVM(settingsParam, this.extensionInstances, profilingEnabled);
684707
}
685708

686709
/**

src/main/java/io/jawk/Cli.java

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import java.io.ObjectOutputStream;
3535
import java.io.PrintStream;
3636
import java.io.StringReader;
37+
import java.io.UncheckedIOException;
3738
import java.util.ArrayList;
3839
import java.util.Collections;
3940
import java.util.LinkedHashMap;
@@ -92,6 +93,8 @@ public final class Cli {
9293
private boolean printUsage;
9394
private boolean sandbox;
9495
private boolean disableOptimize;
96+
private boolean profiling;
97+
private File profilingOutputFile;
9598
private File persistentMemoryFile;
9699

97100
/**
@@ -163,6 +166,15 @@ public boolean isDisableOptimize() {
163166
return disableOptimize;
164167
}
165168

169+
/**
170+
* Indicates whether runtime profiling was requested.
171+
*
172+
* @return {@code true} when profiling should be enabled
173+
*/
174+
public boolean isProfiling() {
175+
return profiling;
176+
}
177+
166178
/**
167179
* Returns the list of script sources specified on the command line.
168180
*
@@ -276,6 +288,17 @@ public void parse(String[] args) {
276288
} else if (arg.equals("-s") || arg.equals("--no-optimize")) {
277289
// -s/--no-optimize : skip tuple queue optimizations
278290
disableOptimize = true;
291+
} else if (arg.equals("--profile")) {
292+
// --profile : collect and print runtime profiling statistics
293+
profiling = true;
294+
} else if (arg.startsWith("--profile=")) {
295+
// --profile=filename : collect profiling statistics and write them to a file
296+
String file = arg.substring("--profile=".length());
297+
if (file.length() == 0) {
298+
throw new IllegalArgumentException("Need output filename for --profile");
299+
}
300+
profiling = true;
301+
profilingOutputFile = new File(file);
279302
} else if (arg.equals("--dump-intermediate")) {
280303
// --dump-intermediate : dump intermediate tuples to file
281304
dumpIntermediateCode = true;
@@ -470,7 +493,7 @@ private File resolvePersistentMemoryFile() {
470493
*/
471494
private void executeProgram(Awk awk, AwkProgram program, File memoryFile) throws Exception {
472495
OutputStreamAwkSink sink = new OutputStreamAwkSink(out, settings.getLocale());
473-
try (AVM avm = awk.createAvm()) {
496+
try (AVM avm = awk.createAvm(profiling)) {
474497
avm.setAwkSink(sink);
475498
avm.setErrorStream(err);
476499
if (memoryFile != null) {
@@ -493,7 +516,36 @@ private void executeProgram(Awk awk, AwkProgram program, File memoryFile) throws
493516
if (memoryFile != null) {
494517
savePersistentMemory(avm, memoryFile);
495518
}
519+
if (profiling) {
520+
printProfilingReport(avm);
521+
}
522+
}
523+
}
524+
}
525+
526+
private void printProfilingReport(AVM avm) {
527+
if (profilingOutputFile == null) {
528+
err.println();
529+
avm.getProfilingReport().print(err);
530+
return;
531+
}
532+
File parent = profilingOutputFile.getAbsoluteFile().getParentFile();
533+
if (parent != null && !parent.isDirectory() && !parent.mkdirs() && !parent.isDirectory()) {
534+
String message = "Failed to create directory '" + parent + "' for profiling report.";
535+
throw new UncheckedIOException(message, new IOException(message));
536+
}
537+
try (PrintStream profileOut = new PrintStream(profilingOutputFile, "UTF-8")) {
538+
avm.getProfilingReport().print(profileOut);
539+
profileOut.flush();
540+
if (profileOut.checkError()) {
541+
throw new UncheckedIOException(
542+
"Failed to write profiling report '" + profilingOutputFile + "'.",
543+
new IOException("PrintStream reported an error"));
496544
}
545+
} catch (IOException ex) {
546+
throw new UncheckedIOException(
547+
"Failed to write profiling report '" + profilingOutputFile + "': " + ex.getMessage(),
548+
ex);
497549
}
498550
}
499551

@@ -571,6 +623,7 @@ private static void usage(PrintStream dest) {
571623
" [--dump-syntax]" +
572624
" [--dump-intermediate]" +
573625
" [-s|--no-optimize]" +
626+
" [--profile[=filename]]" +
574627
" [--locale locale]" +
575628
" [-t]" +
576629
" [-l extension]..." +
@@ -605,6 +658,9 @@ private static void usage(PrintStream dest) {
605658
dest.println(" --dump-syntax = Print the syntax tree.");
606659
dest.println(" --dump-intermediate = Print the intermediate code.");
607660
dest.println(" -s, --no-optimize = (extension) Disable optimizations during compilation.");
661+
dest
662+
.println(
663+
" --profile[=filename] = (extension) Print tuple and function execution statistics to stderr or file.");
608664
dest.println(" --locale Locale = (extension) Specify a locale to be used instead of US-English");
609665
dest.println(" --list-ext = (extension) List available extensions.");
610666
dest.println();
@@ -665,6 +721,9 @@ public static void main(String[] args) {
665721
System.err.println("Failed to parse arguments. Please see the help/usage output (cmd line switch '-h').");
666722
e.printStackTrace(System.err);
667723
System.exit(1);
724+
} catch (UncheckedIOException e) {
725+
System.err.printf("%s: %s%n", e.getClass().getSimpleName(), e.getMessage());
726+
System.exit(1);
668727
} catch (Exception e) {
669728
System.err.printf("%s: %s%n", e.getClass().getSimpleName(), e.getMessage());
670729
System.exit(1);

src/main/java/io/jawk/SandboxedAwk.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,19 @@ public AVM createAvm() {
9696
return createAvm(getSettings());
9797
}
9898

99+
@Override
100+
public AVM createAvm(boolean profilingEnabled) {
101+
return createAvm(getSettings(), profilingEnabled);
102+
}
103+
99104
@Override
100105
protected AVM createAvm(AwkSettings settingsParam) {
101-
return new SandboxedAVM(settingsParam, getExtensionInstances());
106+
return createAvm(settingsParam, false);
107+
}
108+
109+
@Override
110+
protected AVM createAvm(AwkSettings settingsParam, boolean profilingEnabled) {
111+
return new SandboxedAVM(settingsParam, getExtensionInstances(), profilingEnabled);
102112
}
103113
}
104114

src/main/java/io/jawk/backend/AVM.java

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ private void push(Object o) {
133133
}
134134

135135
private final AwkSettings settings;
136+
private final boolean profiling;
137+
private final Map<Opcode, ProfilingReport.Accumulator> tupleProfilingStats;
138+
private final Map<String, ProfilingReport.Accumulator> functionProfilingStats;
139+
private final Deque<ActiveFunction> activeProfilingFunctions;
136140
private boolean inputSourceFilelistAssignmentsApplied;
137141
private InputSource resolvedInputSource;
138142
private AwkExpression installedEvalExpression;
@@ -158,9 +162,34 @@ public AVM() {
158162
*/
159163
public AVM(final AwkSettings parameters,
160164
final Map<String, JawkExtension> extensionInstances) {
165+
this(parameters, extensionInstances, false);
166+
}
167+
168+
/**
169+
* Construct the interpreter, optionally enabling runtime profiling.
170+
*
171+
* @param parameters The parameters affecting the behavior of the
172+
* interpreter.
173+
* @param extensionInstances Map of the extensions to load
174+
* @param profilingEnabled Whether to collect profiling statistics
175+
*/
176+
public AVM(
177+
final AwkSettings parameters,
178+
final Map<String, JawkExtension> extensionInstances,
179+
final boolean profilingEnabled) {
161180
this.settings = parameters != null ? parameters : AwkSettings.DEFAULT_SETTINGS;
162181
this.extensionInstances = extensionInstances == null ?
163182
Collections.<String, JawkExtension>emptyMap() : extensionInstances;
183+
this.profiling = profilingEnabled;
184+
if (profilingEnabled) {
185+
this.tupleProfilingStats = new java.util.EnumMap<Opcode, ProfilingReport.Accumulator>(Opcode.class);
186+
this.functionProfilingStats = new LinkedHashMap<String, ProfilingReport.Accumulator>();
187+
this.activeProfilingFunctions = new ArrayDeque<ActiveFunction>();
188+
} else {
189+
this.tupleProfilingStats = null;
190+
this.functionProfilingStats = null;
191+
this.activeProfilingFunctions = null;
192+
}
164193

165194
arguments = Collections.emptyList();
166195
sortedArrayKeys = this.settings.isUseSortedArrayKeys();
@@ -975,10 +1004,14 @@ private void executeTuples(PositionTracker position)
9751004
IOException {
9761005
Map<Integer, ConditionPair> conditionPairs = null;
9771006
Opcode opcode = null;
1007+
long tupleStartNanos = 0L;
9781008
try {
9791009
while (!position.isEOF()) {
9801010
// System_out.println("--> "+position);
9811011
opcode = position.opcode();
1012+
if (profiling) {
1013+
tupleStartNanos = beforeProfiledTuple(position, opcode);
1014+
}
9821015
// switch on OPCODE
9831016
switch (opcode) {
9841017
case PRINT: {
@@ -2217,7 +2250,8 @@ private void executeTuples(PositionTracker position)
22172250
if (!position.classArg().isInstance(o)) {
22182251
throw new AwkRuntimeException(
22192252
position.lineNumber(),
2220-
"Verification failed. Top-of-stack = " + o.getClass() + " isn't an instance of " + position.classArg());
2253+
"Verification failed. Top-of-stack = " + o.getClass() + " isn't an instance of "
2254+
+ position.classArg());
22212255
}
22222256
push(o);
22232257
position.next();
@@ -2811,8 +2845,16 @@ private void executeTuples(PositionTracker position)
28112845
default:
28122846
throw new Error("invalid opcode: " + position.opcode());
28132847
}
2848+
if (profiling) {
2849+
afterProfiledTuple(opcode, tupleStartNanos);
2850+
}
28142851
}
28152852

2853+
} catch (ExitException ee) {
2854+
if (profiling && (opcode == Opcode.EXIT_WITH_CODE || opcode == Opcode.EXIT_WITHOUT_CODE)) {
2855+
afterProfiledTuple(opcode, tupleStartNanos);
2856+
}
2857+
throw ee;
28162858
} catch (IOException ioe) {
28172859
// clear runtime stack
28182860
runtimeStack.popAllFrames();
@@ -2842,6 +2884,86 @@ private void executeTuples(PositionTracker position)
28422884
}
28432885
}
28442886

2887+
/**
2888+
* Clears all collected profiling statistics.
2889+
*/
2890+
public void resetProfiling() {
2891+
if (!profiling) {
2892+
return;
2893+
}
2894+
tupleProfilingStats.clear();
2895+
functionProfilingStats.clear();
2896+
activeProfilingFunctions.clear();
2897+
}
2898+
2899+
/**
2900+
* Returns an immutable snapshot of the collected profiling statistics.
2901+
*
2902+
* @return profiling report snapshot
2903+
*/
2904+
public ProfilingReport getProfilingReport() {
2905+
if (!profiling) {
2906+
return ProfilingReport.empty();
2907+
}
2908+
return new ProfilingReport(tupleProfilingStats, functionProfilingStats);
2909+
}
2910+
2911+
private long beforeProfiledTuple(PositionTracker position, Opcode opcode) {
2912+
long now = System.nanoTime();
2913+
if (opcode == Opcode.CALL_FUNCTION) {
2914+
activeProfilingFunctions.push(new ActiveFunction(position.stringArg(1), now));
2915+
} else if (opcode == Opcode.EXTENSION) {
2916+
ExtensionFunction function = position.extensionFunctionArg();
2917+
activeProfilingFunctions.push(new ActiveFunction(function.getKeyword(), now));
2918+
}
2919+
return now;
2920+
}
2921+
2922+
private void afterProfiledTuple(Opcode opcode, long tupleStartNanos) {
2923+
long now = System.nanoTime();
2924+
statisticsFor(tupleProfilingStats, opcode).add(now - tupleStartNanos);
2925+
if (opcode == Opcode.EXIT_WITH_CODE || opcode == Opcode.EXIT_WITHOUT_CODE) {
2926+
recordAllFunctionExits(now);
2927+
} else if (opcode == Opcode.EXTENSION || opcode == Opcode.RETURN_FROM_FUNCTION) {
2928+
recordFunctionExit(now);
2929+
}
2930+
}
2931+
2932+
private static <K> ProfilingReport.Accumulator statisticsFor(
2933+
Map<K, ProfilingReport.Accumulator> stats,
2934+
K key) {
2935+
ProfilingReport.Accumulator accumulator = stats.get(key);
2936+
if (accumulator == null) {
2937+
accumulator = new ProfilingReport.Accumulator();
2938+
stats.put(key, accumulator);
2939+
}
2940+
return accumulator;
2941+
}
2942+
2943+
private void recordFunctionExit(long now) {
2944+
if (activeProfilingFunctions.isEmpty()) {
2945+
return;
2946+
}
2947+
ActiveFunction function = activeProfilingFunctions.pop();
2948+
statisticsFor(functionProfilingStats, function.name).add(now - function.startNanos);
2949+
}
2950+
2951+
private void recordAllFunctionExits(long now) {
2952+
while (!activeProfilingFunctions.isEmpty()) {
2953+
recordFunctionExit(now);
2954+
}
2955+
}
2956+
2957+
private static final class ActiveFunction {
2958+
private final String name;
2959+
private final long startNanos;
2960+
2961+
private ActiveFunction(String name, long startNanos) {
2962+
this.name = name;
2963+
this.startNanos = startNanos;
2964+
}
2965+
}
2966+
28452967
/**
28462968
* Releases any prepared input source and runtime I/O resources owned by this
28472969
* AVM.

0 commit comments

Comments
 (0)