diff --git a/core/src/main/java/org/teavm/backend/wasm/debug/info/ControlFlowInfo.java b/core/src/main/java/org/teavm/backend/wasm/debug/info/ControlFlowInfo.java index ada47104d..a4336d912 100644 --- a/core/src/main/java/org/teavm/backend/wasm/debug/info/ControlFlowInfo.java +++ b/core/src/main/java/org/teavm/backend/wasm/debug/info/ControlFlowInfo.java @@ -51,7 +51,7 @@ public class ControlFlowInfo { for (int i = 0; i < functions.size(); ++i) { var range = functions.get(i); out.println("Range #" + i + ": [" + range.start() + ".." + range.end() + ")"); - for (var iter = range.iterator(); iter.hasNext(); iter.next()) { + for (var iter = range.iterator(0); iter.hasNext(); iter.next()) { out.print(" " + Integer.toHexString(iter.address())); if (iter.isCall()) { out.print(" (call)"); diff --git a/core/src/main/java/org/teavm/backend/wasm/debug/info/FunctionControlFlow.java b/core/src/main/java/org/teavm/backend/wasm/debug/info/FunctionControlFlow.java index aa43442cf..d31e35a18 100644 --- a/core/src/main/java/org/teavm/backend/wasm/debug/info/FunctionControlFlow.java +++ b/core/src/main/java/org/teavm/backend/wasm/debug/info/FunctionControlFlow.java @@ -21,7 +21,9 @@ public class FunctionControlFlow { int[] offsets; int[] data; - FunctionControlFlow(int[] offsets, int[] data) { + FunctionControlFlow(int start, int end, int[] offsets, int[] data) { + this.start = start; + this.end = end; this.offsets = offsets; this.data = data; } @@ -34,7 +36,33 @@ public class FunctionControlFlow { return end; } - public FunctionControlFlowIterator iterator() { - return new FunctionControlFlowIterator(this); + public FunctionControlFlowIterator iterator(int index) { + return new FunctionControlFlowIterator(this, index); + } + + public int count() { + return offsets.length; + } + + public int findIndex(int address) { + var l = 0; + var u = offsets.length; + while (true) { + var i = (l + u) / 2; + var t = data[offsets[i]]; + if (address == t) { + return i; + } else if (address > t) { + l = i + 1; + if (l > u) { + return i + 1; + } + } else { + u = i - 1; + if (u < l) { + return i; + } + } + } } } diff --git a/core/src/main/java/org/teavm/backend/wasm/debug/info/FunctionControlFlowBuilder.java b/core/src/main/java/org/teavm/backend/wasm/debug/info/FunctionControlFlowBuilder.java index 56849275f..23b1582f4 100644 --- a/core/src/main/java/org/teavm/backend/wasm/debug/info/FunctionControlFlowBuilder.java +++ b/core/src/main/java/org/teavm/backend/wasm/debug/info/FunctionControlFlowBuilder.java @@ -18,9 +18,16 @@ package org.teavm.backend.wasm.debug.info; import com.carrotsearch.hppc.IntArrayList; public class FunctionControlFlowBuilder { + private int start; + private int end; private IntArrayList offsets = new IntArrayList(); private IntArrayList data = new IntArrayList(); + public FunctionControlFlowBuilder(int start, int end) { + this.start = start; + this.end = end; + } + public void addBranch(int position, int[] targets) { offsets.add(data.size() << 1); data.add(position); @@ -38,6 +45,6 @@ public class FunctionControlFlowBuilder { } public FunctionControlFlow build() { - return new FunctionControlFlow(offsets.toArray(), data.toArray()); + return new FunctionControlFlow(start, end, offsets.toArray(), data.toArray()); } } diff --git a/core/src/main/java/org/teavm/backend/wasm/debug/info/FunctionControlFlowIterator.java b/core/src/main/java/org/teavm/backend/wasm/debug/info/FunctionControlFlowIterator.java index 547c7d796..d0fa2949d 100644 --- a/core/src/main/java/org/teavm/backend/wasm/debug/info/FunctionControlFlowIterator.java +++ b/core/src/main/java/org/teavm/backend/wasm/debug/info/FunctionControlFlowIterator.java @@ -24,8 +24,9 @@ public class FunctionControlFlowIterator { private int offset; private boolean isCall; - FunctionControlFlowIterator(FunctionControlFlow controlFlow) { + FunctionControlFlowIterator(FunctionControlFlow controlFlow, int index) { this.controlFlow = controlFlow; + this.index = 0; } public boolean hasNext() { @@ -37,6 +38,10 @@ public class FunctionControlFlowIterator { valid = false; } + public void rewind(int index) { + this.index = index; + } + private void fill() { if (!valid) { valid = true; diff --git a/core/src/main/java/org/teavm/backend/wasm/debug/info/LineInfoUnpackedSequence.java b/core/src/main/java/org/teavm/backend/wasm/debug/info/LineInfoUnpackedSequence.java index b610af55d..800412390 100644 --- a/core/src/main/java/org/teavm/backend/wasm/debug/info/LineInfoUnpackedSequence.java +++ b/core/src/main/java/org/teavm/backend/wasm/debug/info/LineInfoUnpackedSequence.java @@ -49,13 +49,18 @@ public class LineInfoUnpackedSequence { } public InstructionLocation find(int address) { + var index = findIndex(address); + return index >= 0 ? locations.get(index) : null; + } + + public int findIndex(int address) { if (address < startAddress || address >= endAddress) { - return null; + return -1; } var index = CollectionUtil.binarySearch(locations, address, InstructionLocation::address); if (index < 0) { index = -index; } - return locations.get(Math.min(locations.size() - 1, index)); + return Math.min(locations.size() - 1, index); } } diff --git a/core/src/main/java/org/teavm/backend/wasm/debug/info/StepLocationsFinder.java b/core/src/main/java/org/teavm/backend/wasm/debug/info/StepLocationsFinder.java new file mode 100644 index 000000000..fcd449be1 --- /dev/null +++ b/core/src/main/java/org/teavm/backend/wasm/debug/info/StepLocationsFinder.java @@ -0,0 +1,223 @@ +/* + * Copyright 2022 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.backend.wasm.debug.info; + +import com.carrotsearch.hppc.IntArrayDeque; +import com.carrotsearch.hppc.IntHashSet; +import com.carrotsearch.hppc.IntSet; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.teavm.backend.wasm.debug.parser.DebugInfoParser; +import org.teavm.common.ByteArrayAsyncInputStream; +import org.teavm.common.CollectionUtil; + +public class StepLocationsFinder { + public final DebugInfo debugInfo; + private LineInfoSequence lines; + private FunctionControlFlow graph; + private List points; + private IntArrayDeque queue = new IntArrayDeque(); + private String currentFileName; + private int currentLine; + private boolean enterMethod; + private IntSet breakpointAddresses = new IntHashSet(); + private IntSet callAddresses = new IntHashSet(); + private IntSet visited = new IntHashSet(); + + public StepLocationsFinder(DebugInfo debugInfo) { + this.debugInfo = debugInfo; + } + + public boolean step(String fileName, int line, int address, boolean enterMethod) { + address -= debugInfo.offset(); + updatePoints(address); + if (points == null) { + return false; + } + + var index = CollectionUtil.binarySearch(points, address, p -> p.address); + if (index < 0) { + index = -index - 2; + } + if (index < 0) { + return false; + } + + this.enterMethod = enterMethod; + currentFileName = fileName; + currentLine = line; + queue.addLast(index); + callAddresses.clear(); + breakpointAddresses.clear(); + while (!queue.isEmpty()) { + processTask(); + } + visited.clear(); + currentFileName = null; + + return true; + } + + public int[] getBreakpointAddresses() { + return breakpointAddresses.toArray(); + } + + public int[] getCallAddresses() { + return callAddresses.toArray(); + } + + private void updatePoints(int address) { + var lines = debugInfo.lines().find(address); + var graph = debugInfo.controlFlow().find(address); + if (lines == null || graph == null) { + this.lines = null; + this.graph = null; + points = null; + return; + } + + if (lines != this.lines || graph != this.graph) { + this.lines = lines; + this.graph = graph; + points = null; + } + if (points == null) { + points = createPoints(); + } + } + + private List createPoints() { + var list = new ArrayList(); + var indexInLines = 0; + var graphIter = graph.iterator(0); + var commandExecutor = new LineInfoCommandExecutor(); + + while (indexInLines < lines.commands().size() && graphIter.hasNext()) { + boolean nextInGraph; + if (graphIter == null) { + nextInGraph = false; + } else if (indexInLines >= lines.commands().size()) { + nextInGraph = true; + } else { + nextInGraph = lines.commands().get(indexInLines).address() >= graphIter.address(); + } + if (nextInGraph) { + var point = new Point(graphIter.address()); + list.add(point); + point.isCall = graphIter.isCall(); + point.next = graphIter.targets(); + graphIter.next(); + } else { + var cmd = lines.commands().get(indexInLines++); + cmd.acceptVisitor(commandExecutor); + var location = commandExecutor.createLocation(); + if (location != null && location.location() != null) { + Point point; + if (!list.isEmpty() && list.get(list.size() - 1).address == cmd.address()) { + point = list.get(list.size() - 1); + } else { + point = new Point(cmd.address()); + list.add(point); + } + point.location = location.location(); + } + } + } + + for (var point : list) { + var next = point.next; + if (next == null) { + continue; + } + int j = 0; + for (int i = 0; i < next.length; ++i) { + var foundIndex = CollectionUtil.binarySearch(list, next[i], p -> p.address); + if (foundIndex < 0) { + foundIndex = -foundIndex - 1; + } + if (foundIndex >= list.size()) { + continue; + } + next[j++] = foundIndex; + } + if (j != next.length) { + if (j == 0) { + point.next = null; + } else { + point.next = Arrays.copyOf(next, j); + } + } + } + + list.trimToSize(); + return list; + } + + private void processTask() { + var index = queue.removeFirst(); + if (!visited.add(index)) { + return; + } + + var point = points.get(index); + if (point.location != null && !isCurrent(point.location)) { + breakpointAddresses.add(point.address + debugInfo.offset()); + return; + } + if (enterMethod) { + breakpointAddresses.add(point.address + debugInfo.offset()); + callAddresses.add(point.address + debugInfo.offset()); + } + if (point.next != null) { + for (var nextIndex : point.next) { + queue.addLast(nextIndex); + } + } else if (index < points.size() - 1) { + queue.addLast(index + 1); + } + } + + private boolean isCurrent(Location loc) { + return loc.line() == currentLine && loc.file().fullName().equals(currentFileName); + } + + private static class Point { + int address; + int[] next; + boolean isCall; + Location location; + + Point(int address) { + this.address = address; + } + } + + public static void main(String[] args) throws IOException { + var file = new File("/home/konsoletyper/prog/apache-tomcat-10.0.4/webapps/wasm/classes.wasm"); + var input = new ByteArrayAsyncInputStream(Files.readAllBytes(file.toPath())); + var parser = new DebugInfoParser(input); + input.readFully(parser::parse); + var debugInfo = parser.getDebugInfo(); + + var finder = new StepLocationsFinder(debugInfo); + finder.step("org/teavm/samples/wasi/WasiTest2.java", 9, 0x943e + debugInfo.offset(), false); + System.out.println(Arrays.toString(finder.getBreakpointAddresses())); + } +} diff --git a/core/src/main/java/org/teavm/backend/wasm/debug/parser/ControlFlowParser.java b/core/src/main/java/org/teavm/backend/wasm/debug/parser/ControlFlowParser.java index 0f1848248..0cddf4738 100644 --- a/core/src/main/java/org/teavm/backend/wasm/debug/parser/ControlFlowParser.java +++ b/core/src/main/java/org/teavm/backend/wasm/debug/parser/ControlFlowParser.java @@ -31,7 +31,7 @@ import org.teavm.backend.wasm.parser.Opcode; public class ControlFlowParser implements CodeSectionListener, CodeListener, AddressListener { private int previousAddress; private int address; - private FunctionControlFlowBuilder cfb; + private int startAddress; private List branches = new ArrayList<>(); private List ranges = new ArrayList<>(); private List pendingBranches = new ArrayList<>(); @@ -50,7 +50,7 @@ public class ControlFlowParser implements CodeSectionListener, CodeListener, Add @Override public boolean functionStart(int index, int size) { - cfb = new FunctionControlFlowBuilder(); + startAddress = address; return true; } @@ -71,7 +71,7 @@ public class ControlFlowParser implements CodeSectionListener, CodeListener, Add private int startBlock(boolean loop) { var token = blocks.size(); - var branch = !loop ? newBranch(false) : null; + var branch = !loop ? newPendingBranch(false) : null; var block = new Block(branch, address); blocks.add(block); if (branch != null) { @@ -169,6 +169,7 @@ public class ControlFlowParser implements CodeSectionListener, CodeListener, Add @Override public void functionEnd() { + var cfb = new FunctionControlFlowBuilder(startAddress, address); for (var branch : branches) { if (branch.isCall) { cfb.addCall(branch.address, branch.targets.toArray()); diff --git a/core/src/main/java/org/teavm/debugging/Debugger.java b/core/src/main/java/org/teavm/debugging/Debugger.java index 3ede0404e..78410400f 100644 --- a/core/src/main/java/org/teavm/debugging/Debugger.java +++ b/core/src/main/java/org/teavm/debugging/Debugger.java @@ -15,6 +15,7 @@ */ package org.teavm.debugging; +import com.carrotsearch.hppc.IntHashSet; import java.util.ArrayList; import java.util.Base64; import java.util.Collection; @@ -25,9 +26,11 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Predicate; import org.teavm.backend.wasm.debug.info.DebugInfo; import org.teavm.backend.wasm.debug.info.LineInfoFileCommand; import org.teavm.backend.wasm.debug.info.MethodInfo; +import org.teavm.backend.wasm.debug.info.StepLocationsFinder; import org.teavm.backend.wasm.debug.parser.DebugInfoParser; import org.teavm.common.ByteArrayAsyncInputStream; import org.teavm.common.Promise; @@ -53,6 +56,7 @@ public class Debugger { private JavaScriptDebugger javaScriptDebugger; private DebugInformationProvider debugInformationProvider; private List temporaryBreakpoints = new ArrayList<>(); + private Predicate temporaryBreakpointHandler; private Map debugInformationMap = new HashMap<>(); private Map> debugInformationFileMap = new HashMap<>(); private Map wasmDebugInfoMap = new HashMap<>(); @@ -65,6 +69,7 @@ public class Debugger { private CallFrame[] callStack; private Set scriptNames = new LinkedHashSet<>(); private Set allSourceFiles = new LinkedHashSet<>(); + private StepLocationsFinder wasmStepLocationsFinder; public Debugger(JavaScriptDebugger javaScriptDebugger, DebugInformationProvider debugInformationProvider) { this.javaScriptDebugger = javaScriptDebugger; @@ -113,70 +118,91 @@ public class Debugger { if (callStack == null || callStack.length == 0) { return jsStep(enterMethod); } - var recentFrame = callStack[0]; - if (recentFrame.getLocation() == null || recentFrame.getLocation().getFileName() == null - || recentFrame.getLocation().getLine() < 0) { + var frame = callStack[0]; + if (frame.getLocation() == null || frame.getLocation().getFileName() == null + || frame.getLocation().getLine() < 0) { return jsStep(enterMethod); } var successors = new HashSet(); - boolean first = true; - loop: - for (var frame : callStack) { - var script = frame.getOriginalLocation().getScript(); + var script = frame.getOriginalLocation().getScript(); + var hasSuccessors = false; + if (frame.getLocation() != null && frame.getLocation().getFileName() != null + && frame.getLocation().getLine() >= 0) { switch (script.getLanguage()) { case JS: - if (!addJsBreakpoints(frame, script, enterMethod, first, successors)) { - break loop; - } + hasSuccessors = addJsBreakpoints(frame, script, enterMethod, successors); break; case WASM: { - var info = wasmDebugInfoMap.get(script); - if (info != null) { - return enterMethod ? javaScriptDebugger.stepInto() : javaScriptDebugger.stepOver(); + var promise = stepWasm(frame, enterMethod); + if (promise != null) { + return promise; } break; } - case UNKNOWN: + default: break; } - enterMethod = false; - first = false; } - var jsBreakpointPromises = new ArrayList>(); - for (var successor : successors) { - jsBreakpointPromises.add(javaScriptDebugger.createBreakpoint(successor) - .thenVoid(temporaryBreakpoints::add)); + if (hasSuccessors) { + return jsStep(enterMethod); + } else { + return createTemporaryBreakpoints(successors, null).thenAsync(v -> javaScriptDebugger.stepOut()); } - return Promise.allVoid(jsBreakpointPromises).thenAsync(v -> javaScriptDebugger.resume()); } - private boolean addJsBreakpoints(CallFrame frame, JavaScriptScript script, boolean enterMethod, boolean first, + private Promise createTemporaryBreakpoints(Collection locations, + Predicate handler) { + var jsBreakpointPromises = new ArrayList>(); + for (var location : locations) { + jsBreakpointPromises.add(javaScriptDebugger.createBreakpoint(location) + .thenVoid(temporaryBreakpoints::add)); + } + temporaryBreakpointHandler = handler; + return Promise.allVoid(jsBreakpointPromises); + } + + private boolean addJsBreakpoints(CallFrame frame, JavaScriptScript script, boolean enterMethod, Set successors) { var debugInfo = debugInformationMap.get(script); - boolean exits; - if (frame.getLocation() != null && frame.getLocation().getFileName() != null - && frame.getLocation().getLine() >= 0 && debugInfo != null) { - exits = addFollowing(debugInfo, frame.getLocation(), script, new HashSet<>(), successors); - if (enterMethod) { - var successorFinder = new CallSiteSuccessorFinder(debugInfo, script, successors); - var callSites = debugInfo.getCallSites(frame.getLocation()); - for (var callSite : callSites) { - callSite.acceptVisitor(successorFinder); - } - } - } else { - exits = true; - } - if (!exits) { + if (debugInfo == null) { return false; } - if (!first && frame.getLocation() != null) { - for (var location : debugInfo.getGeneratedLocations(frame.getLocation())) { - successors.add(new JavaScriptLocation(script, location.getLine(), location.getColumn())); + addFollowing(debugInfo, frame.getLocation(), script, new HashSet<>(), successors); + if (enterMethod) { + var successorFinder = new CallSiteSuccessorFinder(debugInfo, script, successors); + var callSites = debugInfo.getCallSites(frame.getLocation()); + for (var callSite : callSites) { + callSite.acceptVisitor(successorFinder); } } - return false; + return true; + } + + private Promise stepWasm(CallFrame frame, boolean enterMethod) { + var debugInfo = wasmDebugInfoMap.get(frame.getOriginalLocation().getScript()); + if (debugInfo == null || debugInfo.controlFlow() == null || debugInfo.lines() == null) { + return null; + } + if (wasmStepLocationsFinder == null || wasmStepLocationsFinder.debugInfo != debugInfo) { + wasmStepLocationsFinder = new StepLocationsFinder(debugInfo); + } + wasmStepLocationsFinder.step(frame.getLocation().getFileName(), frame.getLocation().getLine(), + frame.getOriginalLocation().getColumn(), enterMethod); + + var locations = new ArrayList(); + for (var breakpointAddress : wasmStepLocationsFinder.getBreakpointAddresses()) { + locations.add(new JavaScriptLocation(frame.getOriginalLocation().getScript(), 0, breakpointAddress)); + } + var callAddresses = IntHashSet.from(wasmStepLocationsFinder.getCallAddresses()); + var result = createTemporaryBreakpoints(locations, br -> { + if (br != null && br.isValid() && callAddresses.contains(br.getLocation().getColumn())) { + javaScriptDebugger.stepInto(); + return false; + } + return true; + }); + return result.thenVoid(x -> javaScriptDebugger.stepOut()); } static class CallSiteSuccessorFinder implements DebuggerCallSiteVisitor { @@ -599,20 +625,29 @@ public class Debugger { } private void firePaused(JavaScriptBreakpoint breakpoint) { - List temporaryBreakpoints = new ArrayList<>(this.temporaryBreakpoints); + var temporaryBreakpoints = new ArrayList<>(this.temporaryBreakpoints); + var handler = temporaryBreakpointHandler; this.temporaryBreakpoints.clear(); - List> promises = new ArrayList<>(); - for (JavaScriptBreakpoint jsBreakpoint : temporaryBreakpoints) { + temporaryBreakpointHandler = null; + var promises = new ArrayList>(); + for (var jsBreakpoint : temporaryBreakpoints) { promises.add(jsBreakpoint.destroy()); } callStack = null; Promise.allVoid(promises).thenVoid(v -> { Breakpoint javaBreakpoint = null; - if (breakpoint != null && !temporaryBreakpoints.contains(breakpoint)) { - javaBreakpoint = breakpointMap.get(breakpoint); + JavaScriptBreakpoint tmpBreakpoint = null; + if (breakpoint != null) { + if (temporaryBreakpoints.contains(breakpoint)) { + tmpBreakpoint = breakpoint; + } else { + javaBreakpoint = breakpointMap.get(breakpoint); + } } - for (DebuggerListener listener : getListeners()) { - listener.paused(javaBreakpoint); + if (handler == null || !handler.test(tmpBreakpoint)) { + for (var listener : getListeners()) { + listener.paused(javaBreakpoint); + } } }); }