diff --git a/src/main/java/io/jawk/backend/AVM.java b/src/main/java/io/jawk/backend/AVM.java index f7144235..194ef256 100644 --- a/src/main/java/io/jawk/backend/AVM.java +++ b/src/main/java/io/jawk/backend/AVM.java @@ -2889,9 +2889,15 @@ private String normalizeIndirectFunctionName(String functionName) { /** * Unwinds every call frame and clears the per-call runtime state, after an - * {@code exit} statement or an abandoned execution. + * {@code exit} or {@code nextfile} statement or an abandoned execution. + * When profiling, the abandoned function calls are recorded as exited at + * this point, so the timing report stays accurate and the active-function + * stack does not leak entries (see #557). */ private void resetCallState() { + if (profiling) { + recordAllFunctionExits(System.nanoTime()); + } runtimeStack.popAllFrames(); elementArgumentReferences.clear(); clearOperandStack(); @@ -4180,8 +4186,7 @@ private void executeNextfile(PositionTracker position) { "`nextfile' cannot be called from an ENDFILE rule"); } // nextfile can be invoked from user-defined functions: unwind them. - runtimeStack.popAllFrames(); - clearOperandStack(); + resetCallState(); if (endFileAddress == null || withinBeginFileBlocks && jrt.hasPendingInputFileError(resolvedInputSource)) { // No ENDFILE rules to run, or the file could not be opened: skip diff --git a/src/site/markdown/behavior-changes.md b/src/site/markdown/behavior-changes.md index aa59fabb..dd4e3035 100644 --- a/src/site/markdown/behavior-changes.md +++ b/src/site/markdown/behavior-changes.md @@ -20,6 +20,12 @@ released version automatically via .github/scripts/stamp-behavior-changes.sh. ## Unreleased +- The `--profile` function timing report now records a user-defined function call abandoned by + `nextfile` as exited when `nextfile` unwinds it, with its actual duration. Previously each such + call leaked an entry on the internal function timing stack (unbounded memory growth on long + profiled runs), and the abandoned calls were either missing from the report or accounted with + wildly wrong durations at program exit + ([#557](https://github.com/jawkio/jawk/issues/557)). - A conditional expression whose result feeds a further operation on a literal — shapes like `(v ? v : 24) * 2`, `-(v ? 5 : 24)`, or `$(v ? 1 : 2)` — now evaluates correctly under the default tuple optimization. Previously the peephole literal fold merged the false branch's diff --git a/src/test/java/io/jawk/CliOptionTest.java b/src/test/java/io/jawk/CliOptionTest.java index fcd28ddb..8efbf2cc 100644 --- a/src/test/java/io/jawk/CliOptionTest.java +++ b/src/test/java/io/jawk/CliOptionTest.java @@ -41,6 +41,8 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.junit.Test; import org.junit.Rule; import org.junit.rules.TemporaryFolder; @@ -118,6 +120,44 @@ public void profileOptionRecordsIndirectUserFunctions() throws Exception { assertTrue(result.errorOutput().contains("typeof")); } + @Test + public void profileOptionRecordsFunctionsUnwoundByNextfile() throws Exception { + AwkTestSupport.TestResult result = AwkTestSupport + .cliTest("CLI --profile records functions unwound by nextfile") + .argument("--profile") + .script("function inner() { nextfile } function outer() { inner() } { outer() }") + .file("f1", "a1\na2\n") + .file("f2", "b1\nb2\n") + .file("f3", "c1\nc2\n") + .operand("{{f1}}", "{{f2}}", "{{f3}}") + .expect("") + .run(); + + result.assertExpected(); + // Each file triggers exactly one outer() -> inner() -> nextfile chain; + // nextfile abandons both calls, which must still be recorded once each. + assertEquals(3, profiledFunctionCount(result.errorOutput(), "inner")); + assertEquals(3, profiledFunctionCount(result.errorOutput(), "outer")); + } + + /** + * Extracts the execution count of one function from a {@code --profile} + * report. + * + * @param report the profiling report text + * @param functionName the function to look up + * @return the reported execution count + */ + private static long profiledFunctionCount(String report, String functionName) { + Matcher matcher = Pattern + .compile("^\\s{2}" + Pattern.quote(functionName) + "\\s+(\\d+)\\s", Pattern.MULTILINE) + .matcher(report); + assertTrue( + "function `" + functionName + "' missing from profiling report:\n" + report, + matcher.find()); + return Long.parseLong(matcher.group(1)); + } + @Test public void profileOptionWithFilenameWritesReportToFile() throws Exception { File profile = tempFolder.newFile("profile.txt");