diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/AsyncJdwpUtils.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/AsyncJdwpUtils.java new file mode 100644 index 000000000..fad2ac223 --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/AsyncJdwpUtils.java @@ -0,0 +1,143 @@ +/******************************************************************************* +* Copyright (c) 2022 Microsoft Corporation and others. +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the Eclipse Public License v1.0 +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v10.html +* +* Contributors: +* Microsoft Corporation - initial API and implementation +*******************************************************************************/ + +package com.microsoft.java.debug.core; + +import static java.util.concurrent.CompletableFuture.allOf; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Supplier; + +public class AsyncJdwpUtils { + /** + * Create a the thread pool to process JDWP tasks. + * JDWP tasks are IO-bounded, so use a relatively large thread pool for JDWP tasks. + */ + public static ExecutorService jdwpThreadPool = Executors.newWorkStealingPool(100); + // public static ExecutorService jdwpThreadPool = Executors.newCachedThreadPool(); + + public static CompletableFuture runAsync(List tasks) { + return runAsync(jdwpThreadPool, tasks.toArray(new Runnable[0])); + } + + public static CompletableFuture runAsync(Runnable... tasks) { + return runAsync(jdwpThreadPool, tasks); + } + + public static CompletableFuture runAsync(Executor executor, List tasks) { + return runAsync(executor, tasks.toArray(new Runnable[0])); + } + + public static CompletableFuture runAsync(Executor executor, Runnable... tasks) { + List> promises = new ArrayList<>(); + for (Runnable task : tasks) { + if (task == null) { + continue; + } + + promises.add(CompletableFuture.runAsync(task, executor)); + } + + return CompletableFuture.allOf(promises.toArray(new CompletableFuture[0])); + } + + public static CompletableFuture supplyAsync(Supplier supplier) { + return supplyAsync(jdwpThreadPool, supplier); + } + + public static CompletableFuture supplyAsync(Executor executor, Supplier supplier) { + return CompletableFuture.supplyAsync(supplier, executor); + } + + public static U await(CompletableFuture future) { + try { + return future.join(); + } catch (CompletionException ex) { + if (ex.getCause() instanceof RuntimeException) { + throw (RuntimeException) ex.getCause(); + } + + throw ex; + } + } + + public static List await(CompletableFuture[] futures) { + List results = new ArrayList<>(); + try { + allOf(futures).join(); + for (CompletableFuture future : futures) { + results.add(await(future)); + } + } catch (CompletionException ex) { + if (ex.getCause() instanceof RuntimeException) { + throw (RuntimeException) ex.getCause(); + } + + throw ex; + } + + return results; + } + + public static List await(List> futures) { + return await((CompletableFuture[]) futures.toArray(new CompletableFuture[0])); + } + + public static CompletableFuture> all(CompletableFuture... futures) { + return allOf(futures).thenApply((res) -> { + List results = new ArrayList<>(); + for (CompletableFuture future : futures) { + results.add(future.join()); + } + + return results; + }); + } + + public static CompletableFuture> all(List> futures) { + return allOf(futures.toArray(new CompletableFuture[0])).thenApply((res) -> { + List results = new ArrayList<>(); + for (CompletableFuture future : futures) { + results.add(future.join()); + } + + return results; + }); + } + + public static CompletableFuture> flatAll(CompletableFuture>... futures) { + return allOf(futures).thenApply((res) -> { + List results = new ArrayList<>(); + for (CompletableFuture> future : futures) { + results.addAll(future.join()); + } + + return results; + }); + } + + public static CompletableFuture> flatAll(List>> futures) { + return allOf(futures.toArray(new CompletableFuture[0])).thenApply((res) -> { + List results = new ArrayList<>(); + for (CompletableFuture> future : futures) { + results.addAll(future.join()); + } + + return results; + }); + } +} diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/Breakpoint.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/Breakpoint.java index a81968ad3..8b865d44f 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/Breakpoint.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/Breakpoint.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -12,11 +12,15 @@ package com.microsoft.java.debug.core; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.concurrent.CompletableFuture; +import com.sun.jdi.AbsentInformationException; import com.sun.jdi.Location; import com.sun.jdi.Method; import com.sun.jdi.ReferenceType; @@ -41,6 +45,8 @@ public class Breakpoint implements IBreakpoint { private HashMap propertyMap = new HashMap<>(); private String methodSignature = null; + private boolean async = false; + Breakpoint(VirtualMachine vm, IEventHub eventHub, String className, int lineNumber) { this(vm, eventHub, className, lineNumber, 0, null); } @@ -69,7 +75,7 @@ public class Breakpoint implements IBreakpoint { } // IDebugResource - private List requests = new ArrayList<>(); + private List requests = Collections.synchronizedList(new ArrayList<>()); private List subscriptions = new ArrayList<>(); @Override @@ -162,6 +168,16 @@ public String getLogMessage() { return this.logMessage; } + @Override + public boolean async() { + return this.async; + } + + @Override + public void setAsync(boolean async) { + this.async = async; + } + @Override public CompletableFuture install() { // It's possible that different class loaders create new class with the same name. @@ -185,8 +201,9 @@ public CompletableFuture install() { || localClassPrepareRequest.equals(debugEvent.event.request()))) .subscribe(debugEvent -> { ClassPrepareEvent event = (ClassPrepareEvent) debugEvent.event; - List newRequests = createBreakpointRequests(event.referenceType(), lineNumber, - hitCount, false); + List newRequests = AsyncJdwpUtils.await( + createBreakpointRequests(event.referenceType(), lineNumber, hitCount, false) + ); requests.addAll(newRequests); if (!newRequests.isEmpty() && !future.isDone()) { this.putProperty("verified", true); @@ -195,126 +212,180 @@ public CompletableFuture install() { }); subscriptions.add(subscription); - List refTypes = vm.classesByName(className); - List newRequests = createBreakpointRequests(refTypes, lineNumber, hitCount, true); - requests.addAll(newRequests); + Runnable resolveRequestsFromExistingClasses = () -> { + List refTypes = vm.classesByName(className); + createBreakpointRequests(refTypes, lineNumber, hitCount, true) + .whenComplete((newRequests, ex) -> { + if (ex != null) { + return; + } + + requests.addAll(newRequests); + if (!newRequests.isEmpty() && !future.isDone()) { + this.putProperty("verified", true); + future.complete(this); + } + }); + }; - if (!newRequests.isEmpty() && !future.isDone()) { - this.putProperty("verified", true); - future.complete(this); + if (async()) { + AsyncJdwpUtils.runAsync(resolveRequestsFromExistingClasses); + } else { + resolveRequestsFromExistingClasses.run(); } return future; } - private static List collectLocations(ReferenceType refType, int lineNumber) { - List locations = new ArrayList<>(); - - try { - locations.addAll(refType.locationsOfLine(lineNumber)); - } catch (Exception e) { - // could be AbsentInformationException or ClassNotPreparedException - // but both are expected so no need to further handle + private CompletableFuture> collectLocations(ReferenceType refType, int lineNumber) { + List>> futures = new ArrayList<>(); + Iterator iter = refType.methods().iterator(); + while (iter.hasNext()) { + Method method = iter.next(); + if (async()) { + futures.add(AsyncJdwpUtils.supplyAsync(() -> findLocaitonsOfLine(method, lineNumber))); + } else { + futures.add(CompletableFuture.completedFuture(findLocaitonsOfLine(method, lineNumber))); + } } - return locations; + return AsyncJdwpUtils.flatAll(futures); } - private static List collectLocations(List refTypes, int lineNumber, boolean includeNestedTypes) { - List locations = new ArrayList<>(); - try { - refTypes.forEach(refType -> { - List newLocations = collectLocations(refType, lineNumber); - if (!newLocations.isEmpty()) { - locations.addAll(newLocations); - } else if (includeNestedTypes) { - // ReferenceType.nestedTypes() will invoke vm.allClasses() to list all loaded classes, - // should avoid using nestedTypes for performance. - for (ReferenceType nestedType : refType.nestedTypes()) { - List nestedLocations = collectLocations(nestedType, lineNumber); - if (!nestedLocations.isEmpty()) { - locations.addAll(nestedLocations); - break; - } + private CompletableFuture> collectLocations(List refTypes, int lineNumber, boolean includeNestedTypes) { + List>> futures = new ArrayList<>(); + refTypes.forEach(refType -> { + futures.add(collectLocations(refType, lineNumber, includeNestedTypes)); + }); + + return AsyncJdwpUtils.flatAll(futures); + } + + private CompletableFuture> collectLocations(ReferenceType refType, int lineNumber, boolean includeNestedTypes) { + return collectLocations(refType, lineNumber).thenCompose((newLocations) -> { + if (!newLocations.isEmpty()) { + return CompletableFuture.completedFuture(newLocations); + } else if (includeNestedTypes) { + // ReferenceType.nestedTypes() will invoke vm.allClasses() to list all loaded classes, + // should avoid using nestedTypes for performance. + for (ReferenceType nestedType : refType.nestedTypes()) { + CompletableFuture> nestedLocationsFuture = collectLocations(nestedType, lineNumber); + List nestedLocations = nestedLocationsFuture.join(); + if (!nestedLocations.isEmpty()) { + return CompletableFuture.completedFuture(nestedLocations); } } - }); - } catch (VMDisconnectedException ex) { - // collect locations operation may be executing while JVM is terminating, thus the VMDisconnectedException may be - // possible, in case of VMDisconnectedException, this method will return an empty array which turns out a valid - // response in vscode, causing no error log in trace. - } + } - return locations; + return CompletableFuture.completedFuture(Collections.emptyList()); + }); } - private static List collectLocations(List refTypes, String nameAndSignature) { - List locations = new ArrayList<>(); + private CompletableFuture> collectLocations(List refTypes, String nameAndSignature) { String[] segments = nameAndSignature.split("#"); - + List> futures = new ArrayList<>(); for (ReferenceType refType : refTypes) { - List methods = refType.methods(); - for (Method method : methods) { - if (!method.isAbstract() && !method.isNative() - && segments[0].equals(method.name()) - && (segments[1].equals(method.genericSignature()) || segments[1].equals(method.signature()))) { - locations.add(method.location()); - break; - } + if (async()) { + futures.add(AsyncJdwpUtils.supplyAsync(() -> findMethodLocaiton(refType, segments[0], segments[1]))); + } else { + futures.add(CompletableFuture.completedFuture(findMethodLocaiton(refType, segments[0], segments[1]))); } } - return locations; + + return AsyncJdwpUtils.all(futures); } - private List createBreakpointRequests(ReferenceType refType, int lineNumber, int hitCount, + private Location findMethodLocaiton(ReferenceType refType, String methodName, String methodSiguature) { + List methods = refType.methods(); + Location location = null; + for (Method method : methods) { + if (!method.isAbstract() && !method.isNative() + && methodName.equals(method.name()) + && (methodSiguature.equals(method.genericSignature()) || methodSiguature.equals(method.signature()))) { + location = method.location(); + break; + } + } + + return location; + } + + private List findLocaitonsOfLine(Method method, int lineNumber) { + try { + return method.locationsOfLine(lineNumber); + } catch (AbsentInformationException e) { + // could be AbsentInformationException or ClassNotPreparedException + // but both are expected so no need to further handle + } + + return Collections.emptyList(); + } + + private CompletableFuture> createBreakpointRequests(ReferenceType refType, int lineNumber, int hitCount, boolean includeNestedTypes) { - List refTypes = new ArrayList<>(); - refTypes.add(refType); - return createBreakpointRequests(refTypes, lineNumber, hitCount, includeNestedTypes); + return createBreakpointRequests(Arrays.asList(refType), lineNumber, hitCount, includeNestedTypes); } - private List createBreakpointRequests(List refTypes, int lineNumber, + private CompletableFuture> createBreakpointRequests(List refTypes, int lineNumber, int hitCount, boolean includeNestedTypes) { - List locations; + CompletableFuture> locationsFuture; if (this.methodSignature != null) { - locations = collectLocations(refTypes, this.methodSignature); + locationsFuture = collectLocations(refTypes, this.methodSignature); } else { - locations = collectLocations(refTypes, lineNumber, includeNestedTypes); + locationsFuture = collectLocations(refTypes, lineNumber, includeNestedTypes); } - // find out the existing breakpoint locations - List existingLocations = new ArrayList<>(requests.size()); - Observable.fromIterable(requests).filter(request -> request instanceof BreakpointRequest) - .map(request -> ((BreakpointRequest) request).location()).toList().subscribe(list -> { - existingLocations.addAll(list); - }); - - // remove duplicated locations - List newLocations = new ArrayList<>(locations.size()); - Observable.fromIterable(locations).filter(location -> !existingLocations.contains(location)).toList().subscribe(list -> { - newLocations.addAll(list); - }); + return locationsFuture.thenCompose((locations) -> { + // find out the existing breakpoint locations + List existingLocations = new ArrayList<>(requests.size()); + Observable.fromIterable(requests).filter(request -> request instanceof BreakpointRequest) + .map(request -> ((BreakpointRequest) request).location()).toList().subscribe(list -> { + existingLocations.addAll(list); + }); + + // remove duplicated locations + List newLocations = new ArrayList<>(locations.size()); + Observable.fromIterable(locations).filter(location -> !existingLocations.contains(location)).toList().subscribe(list -> { + newLocations.addAll(list); + }); - List newRequests = new ArrayList<>(newLocations.size()); + List newRequests = new ArrayList<>(newLocations.size()); - newLocations.forEach(location -> { - try { + newLocations.forEach(location -> { BreakpointRequest request = vm.eventRequestManager().createBreakpointRequest(location); request.setSuspendPolicy(BreakpointRequest.SUSPEND_EVENT_THREAD); if (hitCount > 0) { request.addCountFilter(hitCount); } - request.enable(); request.putProperty(IBreakpoint.REQUEST_TYPE, computeRequestType()); newRequests.add(request); - } catch (VMDisconnectedException ex) { - // enable breakpoint operation may be executing while JVM is terminating, thus the VMDisconnectedException may be - // possible, in case of VMDisconnectedException, this method will return an empty array which turns out a valid - // response in vscode, causing no error log in trace. + }); + + List> futures = new ArrayList<>(); + for (BreakpointRequest request : newRequests) { + if (async()) { + futures.add(AsyncJdwpUtils.runAsync(() -> { + try { + request.enable(); + } catch (VMDisconnectedException ex) { + // enable breakpoint operation may be executing while JVM is terminating, thus the VMDisconnectedException may be + // possible, in case of VMDisconnectedException, this method will return an empty array which turns out a valid + // response in vscode, causing no error log in trace. + } + })); + } else { + try { + request.enable(); + } catch (VMDisconnectedException ex) { + // enable breakpoint operation may be executing while JVM is terminating, thus the VMDisconnectedException may be + // possible, in case of VMDisconnectedException, this method will return an empty array which turns out a valid + // response in vscode, causing no error log in trace. + } + } } - }); - return newRequests; + return AsyncJdwpUtils.all(futures).thenApply((res) -> newRequests); + }); } private Object computeRequestType() { diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSession.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSession.java index ab4341f51..220688693 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSession.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSession.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.List; +import com.sun.jdi.ObjectCollectedException; import com.sun.jdi.ThreadReference; import com.sun.jdi.VirtualMachine; import com.sun.jdi.request.EventRequest; @@ -57,8 +58,12 @@ public void resume() { * all threads fully. */ for (ThreadReference tr : DebugUtility.getAllThreadsSafely(this)) { - while (!tr.isCollected() && tr.suspendCount() > 1) { - tr.resume(); + try { + while (tr.suspendCount() > 1) { + tr.resume(); + } + } catch (ObjectCollectedException ex) { + // Skip it if the thread is garbage collected. } } vm.resume(); diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSettings.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSettings.java index e3c928332..f59f10043 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSettings.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSettings.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017-2020 Microsoft Corporation and others. + * Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -43,6 +43,7 @@ public final class DebugSettings { public boolean exceptionFiltersUpdated = false; public int limitOfVariablesPerJdwpRequest = 100; public int jdwpRequestTimeout = 3000; + public AsyncMode asyncJDWP = AsyncMode.OFF; public static DebugSettings getCurrent() { return current; @@ -87,6 +88,15 @@ public static enum HotCodeReplace { NEVER } + public static enum AsyncMode { + @SerializedName("auto") + AUTO, + @SerializedName("on") + ON, + @SerializedName("off") + OFF + } + public static interface IDebugSettingChangeListener { public void update(DebugSettings oldSettings, DebugSettings newSettings); } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugUtility.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugUtility.java index 1202a30f3..4a2a49e9b 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugUtility.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugUtility.java @@ -444,7 +444,7 @@ public static CompletableFuture stopOnEntry(IDebugSession debugSession, St */ public static ThreadReference getThread(IDebugSession debugSession, long threadId) { for (ThreadReference thread : getAllThreadsSafely(debugSession)) { - if (thread.uniqueID() == threadId && !thread.isCollected()) { + if (thread.uniqueID() == threadId) { return thread; } } @@ -477,7 +477,7 @@ public static List getAllThreadsSafely(IDebugSession debugSessi */ public static void resumeThread(ThreadReference thread) { // if thread is not found or is garbage collected, do nothing - if (thread == null || thread.isCollected()) { + if (thread == null) { return; } try { @@ -495,6 +495,25 @@ public static void resumeThread(ThreadReference thread) { } } + public static void resumeThread(ThreadReference thread, int resumeCount) { + if (thread == null) { + return; + } + + try { + for (int i = 0; i < resumeCount; i++) { + /** + * Invoking this method will decrement the count of pending suspends on this thread. + * If it is decremented to 0, the thread will continue to execute. + */ + thread.resume(); + } + } catch (ObjectCollectedException ex) { + // ObjectCollectionException can be thrown if the thread has already completed (exited) in the VM when calling suspendCount, + // the resume operation to this thread is meanness. + } + } + /** * Remove the event request from the vm. If the vm has terminated, do nothing. * @param eventManager diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IBreakpoint.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IBreakpoint.java index 3db16b577..04fbf5005 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IBreakpoint.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IBreakpoint.java @@ -44,4 +44,11 @@ public interface IBreakpoint extends IDebugResource { String getLogMessage(); void setLogMessage(String logMessage); + + default void setAsync(boolean async) { + } + + default boolean async() { + return false; + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IMethodBreakpoint.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IMethodBreakpoint.java index 668884567..68f9539c8 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IMethodBreakpoint.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IMethodBreakpoint.java @@ -30,4 +30,11 @@ public interface IMethodBreakpoint extends IDebugResource { Object getProperty(Object key); void putProperty(Object key, Object value); + + default void setAsync(boolean async) { + } + + default boolean async() { + return false; + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/MethodBreakpoint.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/MethodBreakpoint.java index 82c5b75e2..7a6e74c6b 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/MethodBreakpoint.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/MethodBreakpoint.java @@ -11,6 +11,7 @@ package com.microsoft.java.debug.core; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -42,12 +43,13 @@ public class MethodBreakpoint implements IMethodBreakpoint, IEvaluatableBreakpoi private String functionName; private String condition; private int hitCount; + private boolean async = false; private HashMap propertyMap = new HashMap<>(); private Object compiledConditionalExpression = null; private Map compiledExpressions = new ConcurrentHashMap<>(); - private List requests = new ArrayList<>(); + private List requests = Collections.synchronizedList(new ArrayList<>()); private List subscriptions = new ArrayList<>(); public MethodBreakpoint(VirtualMachine vm, IEventHub eventHub, String className, String functionName, @@ -169,6 +171,16 @@ public void setHitCount(int hitCount) { }); } + @Override + public boolean async() { + return this.async; + } + + @Override + public void setAsync(boolean async) { + this.async = async; + } + @Override public CompletableFuture install() { Disposable subscription = eventHub.events() @@ -194,7 +206,9 @@ public CompletableFuture install() { && (classPrepareRequest.equals(debugEvent.event.request()))) .subscribe(debugEvent -> { ClassPrepareEvent event = (ClassPrepareEvent) debugEvent.event; - Optional createdRequest = createMethodEntryRequest(event.referenceType()); + Optional createdRequest = AsyncJdwpUtils.await( + createMethodEntryRequest(event.referenceType()) + ); if (createdRequest.isPresent()) { MethodEntryRequest methodEntryRequest = createdRequest.get(); requests.add(methodEntryRequest); @@ -206,22 +220,44 @@ public CompletableFuture install() { }); subscriptions.add(subscription); - List types = vm.classesByName(className); - for (ReferenceType type : types) { - Optional createdRequest = createMethodEntryRequest(type); - if (createdRequest.isPresent()) { - MethodEntryRequest methodEntryRequest = createdRequest.get(); - requests.add(methodEntryRequest); - if (!future.isDone()) { - this.putProperty("verified", true); - future.complete(this); - } + Runnable createRequestsFromLoadedClasses = () -> { + List types = vm.classesByName(className); + for (ReferenceType type : types) { + createMethodEntryRequest(type).whenComplete((createdRequest, ex) -> { + if (ex != null) { + return; + } + + if (createdRequest.isPresent()) { + MethodEntryRequest methodEntryRequest = createdRequest.get(); + requests.add(methodEntryRequest); + if (!future.isDone()) { + this.putProperty("verified", true); + future.complete(this); + } + } + }); } + }; + + if (async()) { + AsyncJdwpUtils.runAsync(createRequestsFromLoadedClasses); + } else { + createRequestsFromLoadedClasses.run(); } + return future; } - private Optional createMethodEntryRequest(ReferenceType type) { + private CompletableFuture> createMethodEntryRequest(ReferenceType type) { + if (async()) { + return CompletableFuture.supplyAsync(() -> createMethodEntryRequest0(type)); + } else { + return CompletableFuture.completedFuture(createMethodEntryRequest0(type)); + } + } + + private Optional createMethodEntryRequest0(ReferenceType type) { return type.methodsByName(functionName).stream().findFirst().map(method -> { MethodEntryRequest request = vm.eventRequestManager().createMethodEntryRequest(); diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/UsageDataSession.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/UsageDataSession.java index e292026c8..911dca529 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/UsageDataSession.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/UsageDataSession.java @@ -190,6 +190,12 @@ public static void recordEvent(Event event) { } } + public static void recordInfo(String key, Object value) { + Map map = new HashMap<>(); + map.put(key, value); + usageDataLogger.log(Level.INFO, "session info", map); + } + /** * Record counts for each user errors encountered. */ diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapterContext.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapterContext.java index 30fb12200..4185d7597 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapterContext.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapterContext.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017-2020 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -21,6 +21,7 @@ import com.microsoft.java.debug.core.DebugSettings; import com.microsoft.java.debug.core.IDebugSession; +import com.microsoft.java.debug.core.DebugSettings.AsyncMode; import com.microsoft.java.debug.core.adapter.variables.IVariableFormatter; import com.microsoft.java.debug.core.adapter.variables.VariableFormatterFactory; import com.microsoft.java.debug.core.protocol.IProtocolServer; @@ -57,6 +58,8 @@ public class DebugAdapterContext implements IDebugAdapterContext { private long shellProcessId = -1; private long processId = -1; + private boolean localDebugging = true; + private IdCollection sourceReferences = new IdCollection<>(); private RecyclableObjectPool recyclableIdPool = new RecyclableObjectPool<>(); private IVariableFormatter variableFormatter = VariableFormatterFactory.createVariableFormatter(); @@ -65,6 +68,7 @@ public class DebugAdapterContext implements IDebugAdapterContext { private IExceptionManager exceptionManager = new ExceptionManager(); private IBreakpointManager breakpointManager = new BreakpointManager(); private IStepResultManager stepResultManager = new StepResultManager(); + private ThreadCache threadCache = new ThreadCache(); public DebugAdapterContext(IProtocolServer server, IProviderContext providerContext) { this.providerContext = providerContext; @@ -349,4 +353,27 @@ public void setProcessId(long processId) { public void setShellProcessId(long shellProcessId) { this.shellProcessId = shellProcessId; } + + @Override + public void setThreadCache(ThreadCache cache) { + this.threadCache = cache; + } + + @Override + public ThreadCache getThreadCache() { + return this.threadCache; + } + + @Override + public boolean asyncJDWP() { + return DebugSettings.getCurrent().asyncJDWP == AsyncMode.ON; + } + + public boolean isLocalDebugging() { + return localDebugging; + } + + public void setLocalDebugging(boolean local) { + this.localDebugging = local; + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IDebugAdapterContext.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IDebugAdapterContext.java index a1b21ecee..bfcad8905 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IDebugAdapterContext.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IDebugAdapterContext.java @@ -136,4 +136,14 @@ public interface IDebugAdapterContext { void setProcessId(long processId); long getProcessId(); + + void setThreadCache(ThreadCache cache); + + ThreadCache getThreadCache(); + + boolean asyncJDWP(); + + boolean isLocalDebugging(); + + void setLocalDebugging(boolean local); } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IStackFrameManager.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IStackFrameManager.java index de10319f6..f3ae95d6e 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IStackFrameManager.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IStackFrameManager.java @@ -31,4 +31,14 @@ public interface IStackFrameManager { * @return all the stackframes in the specified thread */ StackFrame[] reloadStackFrames(ThreadReference thread); + + /** + * Refersh the stackframes starting from the specified depth and length. + * + * @param thread the jdi thread + * @param start the index of the first frame to refresh. Index 0 represents the current frame. + * @param length the number of frames to refersh + * @return the refreshed stackframes + */ + StackFrame[] reloadStackFrames(ThreadReference thread, int start, int length); } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/StackFrameManager.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/StackFrameManager.java index 9e1a86970..d2f934901 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/StackFrameManager.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/StackFrameManager.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017 Microsoft Corporation and others. + * Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -11,7 +11,6 @@ package com.microsoft.java.debug.core.adapter; -import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -21,10 +20,10 @@ import com.sun.jdi.ThreadReference; public class StackFrameManager implements IStackFrameManager { - private Map threadStackFrameMap = Collections.synchronizedMap(new HashMap<>()); + private Map threadStackFrameMap = new HashMap<>(); @Override - public StackFrame getStackFrame(StackFrameReference ref) { + public synchronized StackFrame getStackFrame(StackFrameReference ref) { ThreadReference thread = ref.getThread(); int depth = ref.getDepth(); StackFrame[] frames = threadStackFrameMap.get(thread.uniqueID()); @@ -32,13 +31,39 @@ public StackFrame getStackFrame(StackFrameReference ref) { } @Override - public StackFrame[] reloadStackFrames(ThreadReference thread) { + public synchronized StackFrame[] reloadStackFrames(ThreadReference thread) { return threadStackFrameMap.compute(thread.uniqueID(), (key, old) -> { try { - return thread.frames().toArray(new StackFrame[0]); + if (old == null || old.length == 0) { + return thread.frames().toArray(new StackFrame[0]); + } else { + return thread.frames(0, old.length).toArray(new StackFrame[0]); + } } catch (IncompatibleThreadStateException e) { return new StackFrame[0]; } }); } + + @Override + public synchronized StackFrame[] reloadStackFrames(ThreadReference thread, int start, int length) { + long threadId = thread.uniqueID(); + StackFrame[] old = threadStackFrameMap.get(threadId); + try { + StackFrame[] newFrames = thread.frames(start, length).toArray(new StackFrame[0]); + if (old == null || (start == 0 && length == old.length)) { + threadStackFrameMap.put(threadId, newFrames); + } else { + int maxLength = Math.max(old.length, start + length); + StackFrame[] totalFrames = new StackFrame[maxLength]; + System.arraycopy(old, 0, totalFrames, 0, old.length); + System.arraycopy(newFrames, 0, totalFrames, start, length); + threadStackFrameMap.put(threadId, totalFrames); + } + + return newFrames; + } catch (IncompatibleThreadStateException | IndexOutOfBoundsException e) { + return new StackFrame[0]; + } + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ThreadCache.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ThreadCache.java new file mode 100644 index 000000000..3232dbf18 --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ThreadCache.java @@ -0,0 +1,72 @@ +/******************************************************************************* +* Copyright (c) 2022 Microsoft Corporation and others. +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the Eclipse Public License v1.0 +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v10.html +* +* Contributors: +* Microsoft Corporation - initial API and implementation +*******************************************************************************/ + +package com.microsoft.java.debug.core.adapter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.sun.jdi.ThreadReference; + +public class ThreadCache { + private List allThreads = new ArrayList<>(); + private Map threadNameMap = new ConcurrentHashMap<>(); + private Map deathThreads = Collections.synchronizedMap(new LinkedHashMap<>() { + @Override + protected boolean removeEldestEntry(java.util.Map.Entry eldest) { + return this.size() > 100; + } + }); + + public synchronized void resetThreads(List threads) { + allThreads.clear(); + allThreads.addAll(threads); + } + + public synchronized List getThreads() { + return allThreads; + } + + public synchronized ThreadReference getThread(long threadId) { + for (ThreadReference thread : allThreads) { + if (threadId == thread.uniqueID()) { + return thread; + } + } + + return null; + } + + public void setThreadName(long threadId, String name) { + threadNameMap.put(threadId, name); + } + + public String getThreadName(long threadId) { + return threadNameMap.get(threadId); + } + + public void addDeathThread(long threadId) { + threadNameMap.remove(threadId); + deathThreads.put(threadId, true); + } + + public void removeDeathThread(long threadId) { + deathThreads.remove(threadId); + } + + public boolean isDeathThread(long threadId) { + return deathThreads.containsKey(threadId); + } +} diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/AttachRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/AttachRequestHandler.java index 7069bc39e..ecadbaaac 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/AttachRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/AttachRequestHandler.java @@ -23,6 +23,7 @@ import com.microsoft.java.debug.core.Configuration; import com.microsoft.java.debug.core.DebugUtility; import com.microsoft.java.debug.core.IDebugSession; +import com.microsoft.java.debug.core.UsageDataSession; import com.microsoft.java.debug.core.adapter.AdapterUtils; import com.microsoft.java.debug.core.adapter.Constants; import com.microsoft.java.debug.core.adapter.ErrorCode; @@ -39,6 +40,7 @@ import com.microsoft.java.debug.core.protocol.Requests.AttachArguments; import com.microsoft.java.debug.core.protocol.Requests.Command; import com.sun.jdi.connect.IllegalConnectorArgumentsException; +import com.sun.jdi.request.EventRequest; import org.apache.commons.lang3.StringUtils; @@ -58,6 +60,7 @@ public CompletableFuture handle(Command command, Arguments arguments, context.setSourcePaths(attachArguments.sourcePaths); context.setDebuggeeEncoding(StandardCharsets.UTF_8); // Use UTF-8 as debuggee's default encoding format. context.setStepFilters(attachArguments.stepFilters); + context.setLocalDebugging(isLocalHost(attachArguments.hostName)); IVirtualMachineManagerProvider vmProvider = context.getProvider(IVirtualMachineManagerProvider.class); vmHandler.setVmProvider(vmProvider); @@ -96,6 +99,14 @@ public CompletableFuture handle(Command command, Arguments arguments, logger.warning(warnMessage); context.getProtocolServer().sendEvent(Events.OutputEvent.createConsoleOutput(warnMessage)); } + + EventRequest request = debugSession.getVM().eventRequestManager().createVMDeathRequest(); + request.setSuspendPolicy(EventRequest.SUSPEND_NONE); + long sent = System.currentTimeMillis(); + request.enable(); + long received = System.currentTimeMillis(); + logger.info("Network latency for JDWP command: " + (received - sent) + "ms"); + UsageDataSession.recordInfo("networkLatency", (received - sent)); } IEvaluationProvider evaluationProvider = context.getProvider(IEvaluationProvider.class); @@ -111,4 +122,13 @@ public CompletableFuture handle(Command command, Arguments arguments, return CompletableFuture.completedFuture(response); } + private boolean isLocalHost(String hostName) { + if (hostName == null || "localhost".equals(hostName) || "127.0.0.1".equals(hostName)) { + return true; + } + + // TODO: Check the host name of current computer as well. + return false; + } + } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ConfigurationDoneRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ConfigurationDoneRequestHandler.java index 9cd3aa446..311db11c3 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ConfigurationDoneRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ConfigurationDoneRequestHandler.java @@ -55,6 +55,7 @@ public List getTargetCommands() { public CompletableFuture handle(Command command, Arguments arguments, Response response, IDebugAdapterContext context) { IDebugSession debugSession = context.getDebugSession(); vmHandler.setVmProvider(context.getProvider(IVirtualMachineManagerProvider.class)); + UsageDataSession.recordInfo("asyncJDWP", context.asyncJDWP()); if (debugSession != null) { // This is a global event handler to handle the JDI Event from Virtual Machine. debugSession.getEventHub().events().subscribe(debugEvent -> { @@ -104,6 +105,7 @@ private void handleDebugEvent(DebugEvent debugEvent, IDebugSession debugSession, ThreadReference deathThread = ((ThreadDeathEvent) event).thread(); Events.ThreadEvent threadDeathEvent = new Events.ThreadEvent("exited", deathThread.uniqueID()); context.getProtocolServer().sendEvent(threadDeathEvent); + context.getThreadCache().addDeathThread(deathThread.uniqueID()); } else if (event instanceof BreakpointEvent) { // ignore since SetBreakpointsRequestHandler has already handled } else if (event instanceof ExceptionEvent) { diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/EvaluateRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/EvaluateRequestHandler.java index 9cf99742d..1a0e41003 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/EvaluateRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/EvaluateRequestHandler.java @@ -64,6 +64,11 @@ public CompletableFuture handle(Command command, Arguments arguments, VariableUtils.applyFormatterOptions(options, evalArguments.format != null && evalArguments.format.hex); String expression = evalArguments.expression; + // Async mode is supposed to be performant, then disable the advanced features like hover evaluation. + if (!context.isLocalDebugging() && context.asyncJDWP() && "hover".equals(evalArguments.context)) { + return CompletableFuture.completedFuture(response); + } + if (StringUtils.isBlank(expression)) { throw new CompletionException(AdapterUtils.createUserErrorDebugException( "Failed to evaluate. Reason: Empty expression cannot be evaluated.", diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/InlineValuesRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/InlineValuesRequestHandler.java index e9697f899..b30959cd5 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/InlineValuesRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/InlineValuesRequestHandler.java @@ -72,7 +72,7 @@ public List getTargetCommands() { public CompletableFuture handle(Command command, Arguments arguments, Response response, IDebugAdapterContext context) { InlineValuesArguments inlineValuesArgs = (InlineValuesArguments) arguments; - int variableCount = inlineValuesArgs == null || inlineValuesArgs.variables == null ? 0 : inlineValuesArgs.variables.length; + final int variableCount = inlineValuesArgs == null || inlineValuesArgs.variables == null ? 0 : inlineValuesArgs.variables.length; InlineVariable[] inlineVariables = inlineValuesArgs.variables; StackFrameReference stackFrameReference = (StackFrameReference) context.getRecyclableIdPool().getObjectById(inlineValuesArgs.frameId); if (stackFrameReference == null) { @@ -81,6 +81,12 @@ public CompletableFuture handle(Command command, Arguments arguments, return CompletableFuture.completedFuture(response); } + // Async mode is supposed to be performant, then disable the advanced features like inline values. + if (!context.isLocalDebugging() && context.asyncJDWP()) { + response.body = new Responses.InlineValuesResponse(null); + return CompletableFuture.completedFuture(response); + } + IStackFrameManager stackFrameManager = context.getStackFrameManager(); StackFrame frame = stackFrameManager.getStackFrame(stackFrameReference); if (frame == null) { diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/RestartFrameHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/RestartFrameHandler.java index 3479c9a8a..2023209f4 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/RestartFrameHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/RestartFrameHandler.java @@ -30,6 +30,7 @@ import com.microsoft.java.debug.core.protocol.Requests.Arguments; import com.microsoft.java.debug.core.protocol.Requests.Command; import com.microsoft.java.debug.core.protocol.Requests.RestartFrameArguments; +import com.sun.jdi.IncompatibleThreadStateException; import com.sun.jdi.StackFrame; import com.sun.jdi.ThreadReference; import com.sun.jdi.request.StepRequest; @@ -59,7 +60,7 @@ public CompletableFuture handle(Command command, Arguments arguments, if (canRestartFrame(context, stackFrameReference)) { try { ThreadReference reference = stackFrameReference.getThread(); - popStackFrames(context, reference, stackFrameReference.getDepth()); + popStackFrames(context, stackFrameReference); stepInto(context, reference); } catch (DebugException de) { context.getProtocolServer().sendEvent(new Events.UserNotificationEvent(NotificationType.ERROR, de.getMessage())); @@ -80,10 +81,20 @@ private boolean canRestartFrame(IDebugAdapterContext context, StackFrameReferenc return false; } ThreadReference reference = frameReference.getThread(); - StackFrame[] frames = context.getStackFrameManager().reloadStackFrames(reference); + int totalFrames; + try { + totalFrames = reference.frameCount(); + } catch (IncompatibleThreadStateException e) { + return false; + } // The frame cannot be the bottom one of the call stack: - if (frames.length <= frameReference.getDepth() + 1) { + if (totalFrames <= frameReference.getDepth() + 1) { + return false; + } + + StackFrame[] frames = context.getStackFrameManager().reloadStackFrames(reference, 0, frameReference.getDepth() + 2); + if (frames.length == 0) { return false; } @@ -96,9 +107,12 @@ private boolean canRestartFrame(IDebugAdapterContext context, StackFrameReferenc return true; } - private void popStackFrames(IDebugAdapterContext context, ThreadReference thread, int depth) throws DebugException { - StackFrame[] frames = context.getStackFrameManager().reloadStackFrames(thread); - StackFrameUtility.pop(frames[depth]); + private void popStackFrames(IDebugAdapterContext context, StackFrameReference stackFrameRef) throws DebugException { + StackFrame frame = context.getStackFrameManager().getStackFrame(stackFrameRef); + if (frame == null) { + return; + } + StackFrameUtility.pop(frame); } private void stepInto(IDebugAdapterContext context, ThreadReference thread) { diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetBreakpointsRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetBreakpointsRequestHandler.java index 6dc74a0bd..085dfe63f 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetBreakpointsRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetBreakpointsRequestHandler.java @@ -130,10 +130,11 @@ public CompletableFuture handle(Command command, Arguments arguments, IBreakpoint[] added = context.getBreakpointManager() .setBreakpoints(AdapterUtils.decodeURIComponent(sourcePath), toAdds, bpArguments.sourceModified); for (int i = 0; i < bpArguments.breakpoints.length; i++) { + added[i].setAsync(context.asyncJDWP()); // For newly added breakpoint, should install it to debuggee first. if (toAdds[i] == added[i] && added[i].className() != null) { added[i].install().thenAccept(bp -> { - Events.BreakpointEvent bpEvent = new Events.BreakpointEvent("new", this.convertDebuggerBreakpointToClient(bp, context)); + Events.BreakpointEvent bpEvent = new Events.BreakpointEvent("changed", this.convertDebuggerBreakpointToClient(bp, context)); context.getProtocolServer().sendEvent(bpEvent); }); } else if (added[i].className() != null) { diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetFunctionBreakpointsRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetFunctionBreakpointsRequestHandler.java index fa4d23388..3f370b429 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetFunctionBreakpointsRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetFunctionBreakpointsRequestHandler.java @@ -93,6 +93,7 @@ public CompletableFuture handle(Command command, Arguments arguments, continue; } + currentMethodBreakpoints[i].setAsync(context.asyncJDWP()); // If the requested method breakpoint exists in the manager, it will reuse // the cached breakpoint exists object. // Otherwise add the requested method breakpoint to the cache. diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.java index 021bae609..ffa74206e 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -16,11 +16,14 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; +import com.microsoft.java.debug.core.AsyncJdwpUtils; import com.microsoft.java.debug.core.DebugUtility; import com.microsoft.java.debug.core.adapter.AdapterUtils; import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; @@ -36,9 +39,12 @@ import com.microsoft.java.debug.core.protocol.Types; import com.sun.jdi.AbsentInformationException; import com.sun.jdi.IncompatibleThreadStateException; +import com.sun.jdi.LocalVariable; import com.sun.jdi.Location; import com.sun.jdi.Method; import com.sun.jdi.ObjectCollectedException; +import com.sun.jdi.ObjectReference; +import com.sun.jdi.ReferenceType; import com.sun.jdi.StackFrame; import com.sun.jdi.ThreadReference; @@ -57,26 +63,32 @@ public CompletableFuture handle(Command command, Arguments arguments, response.body = new Responses.StackTraceResponseBody(result, 0); return CompletableFuture.completedFuture(response); } - ThreadReference thread = DebugUtility.getThread(context.getDebugSession(), stacktraceArgs.threadId); + ThreadReference thread = context.getThreadCache().getThread(stacktraceArgs.threadId); + if (thread == null) { + thread = DebugUtility.getThread(context.getDebugSession(), stacktraceArgs.threadId); + } int totalFrames = 0; if (thread != null) { try { totalFrames = thread.frameCount(); + int count = stacktraceArgs.levels == 0 ? totalFrames - stacktraceArgs.startFrame + : Math.min(totalFrames - stacktraceArgs.startFrame, stacktraceArgs.levels); if (totalFrames <= stacktraceArgs.startFrame) { response.body = new Responses.StackTraceResponseBody(result, totalFrames); return CompletableFuture.completedFuture(response); } - StackFrame[] frames = context.getStackFrameManager().reloadStackFrames(thread); - int count = stacktraceArgs.levels == 0 ? totalFrames - stacktraceArgs.startFrame - : Math.min(totalFrames - stacktraceArgs.startFrame, stacktraceArgs.levels); - for (int i = stacktraceArgs.startFrame; i < frames.length && count-- > 0; i++) { + StackFrame[] frames = context.getStackFrameManager().reloadStackFrames(thread, stacktraceArgs.startFrame, count); + List jdiFrames = resolveStackFrameInfos(frames, context.asyncJDWP()); + for (int i = stacktraceArgs.startFrame; i < jdiFrames.size() && count-- > 0; i++) { StackFrameReference stackframe = new StackFrameReference(thread, i); - int frameId = context.getRecyclableIdPool().addObject(thread.uniqueID(), stackframe); - result.add(convertDebuggerStackFrameToClient(frames[i], frameId, context)); + int frameId = context.getRecyclableIdPool().addObject(stacktraceArgs.threadId, stackframe); + StackFrameInfo jdiFrame = jdiFrames.get(i - stacktraceArgs.startFrame); + result.add(convertDebuggerStackFrameToClient(jdiFrame, frameId, context)); } } catch (IncompatibleThreadStateException | IndexOutOfBoundsException | URISyntaxException - | AbsentInformationException | ObjectCollectedException e) { + | AbsentInformationException | ObjectCollectedException + | CancellationException | CompletionException e) { // when error happens, the possible reason is: // 1. the vscode has wrong parameter/wrong uri // 2. the thread actually terminates @@ -87,18 +99,79 @@ public CompletableFuture handle(Command command, Arguments arguments, return CompletableFuture.completedFuture(response); } - private Types.StackFrame convertDebuggerStackFrameToClient(StackFrame stackFrame, int frameId, IDebugAdapterContext context) + private static List resolveStackFrameInfos(StackFrame[] frames, boolean async) + throws AbsentInformationException, IncompatibleThreadStateException { + List jdiFrames = new ArrayList<>(); + List> futures = new ArrayList<>(); + for (StackFrame frame : frames) { + StackFrameInfo jdiFrame = new StackFrameInfo(frame); + jdiFrame.location = jdiFrame.frame.location(); + jdiFrame.method = jdiFrame.location.method(); + jdiFrame.methodName = jdiFrame.method.name(); + jdiFrame.isNative = jdiFrame.method.isNative(); + jdiFrame.declaringType = jdiFrame.location.declaringType(); + if (async) { + // JDWP Command: M_LINE_TABLE + futures.add(AsyncJdwpUtils.runAsync(() -> { + jdiFrame.lineNumber = jdiFrame.location.lineNumber(); + })); + + // JDWP Commands: RT_SOURCE_DEBUG_EXTENSION, RT_SOURCE_FILE + futures.add(AsyncJdwpUtils.runAsync(() -> { + try { + // When the .class file doesn't contain source information in meta data, + // invoking Location#sourceName() would throw AbsentInformationException. + jdiFrame.sourceName = jdiFrame.declaringType.sourceName(); + } catch (AbsentInformationException e) { + jdiFrame.sourceName = null; + } + })); + + // JDWP Command: RT_SIGNATURE + futures.add(AsyncJdwpUtils.runAsync(() -> { + jdiFrame.typeSignature = jdiFrame.declaringType.signature(); + })); + } else { + jdiFrame.lineNumber = jdiFrame.location.lineNumber(); + jdiFrame.typeSignature = jdiFrame.declaringType.signature(); + try { + // When the .class file doesn't contain source information in meta data, + // invoking Location#sourceName() would throw AbsentInformationException. + jdiFrame.sourceName = jdiFrame.declaringType.sourceName(); + } catch (AbsentInformationException e) { + jdiFrame.sourceName = null; + } + } + + jdiFrames.add(jdiFrame); + } + + AsyncJdwpUtils.await(futures); + for (StackFrameInfo jdiFrame : jdiFrames) { + jdiFrame.typeName = jdiFrame.declaringType.name(); + jdiFrame.argumentTypeNames = jdiFrame.method.argumentTypeNames(); + if (jdiFrame.sourceName == null) { + String enclosingType = AdapterUtils.parseEnclosingType(jdiFrame.typeName); + jdiFrame.sourceName = enclosingType.substring(enclosingType.lastIndexOf('.') + 1) + ".java"; + jdiFrame.sourcePath = enclosingType.replace('.', File.separatorChar) + ".java"; + } else { + jdiFrame.sourcePath = jdiFrame.declaringType.sourcePaths(null).get(0); + } + } + + return jdiFrames; + } + + private Types.StackFrame convertDebuggerStackFrameToClient(StackFrameInfo jdiFrame, int frameId, IDebugAdapterContext context) throws URISyntaxException, AbsentInformationException { - Location location = stackFrame.location(); - Method method = location.method(); - Types.Source clientSource = this.convertDebuggerSourceToClient(location, context); - String methodName = formatMethodName(method, true, true); - int lineNumber = AdapterUtils.convertLineNumber(location.lineNumber(), context.isDebuggerLinesStartAt1(), context.isClientLinesStartAt1()); + Types.Source clientSource = convertDebuggerSourceToClient(jdiFrame.typeName, jdiFrame.sourceName, jdiFrame.sourcePath, context); + String methodName = formatMethodName(jdiFrame.methodName, jdiFrame.argumentTypeNames, jdiFrame.typeName, true, true); + int clientLineNumber = AdapterUtils.convertLineNumber(jdiFrame.lineNumber, context.isDebuggerLinesStartAt1(), context.isClientLinesStartAt1()); // Line number returns -1 if the information is not available; specifically, always returns -1 for native methods. String presentationHint = null; - if (lineNumber < 0) { + if (clientLineNumber < 0) { presentationHint = "subtle"; - if (method.isNative()) { + if (jdiFrame.isNative) { // For native method, display a tip text "native method" in the Call Stack View. methodName += "[native method]"; } else { @@ -107,25 +180,7 @@ private Types.StackFrame convertDebuggerStackFrameToClient(StackFrame stackFrame clientSource = null; } } - return new Types.StackFrame(frameId, methodName, clientSource, lineNumber, context.isClientColumnsStartAt1() ? 1 : 0, presentationHint); - } - - private Types.Source convertDebuggerSourceToClient(Location location, IDebugAdapterContext context) throws URISyntaxException { - final String fullyQualifiedName = location.declaringType().name(); - String sourceName = ""; - String relativeSourcePath = ""; - try { - // When the .class file doesn't contain source information in meta data, - // invoking Location#sourceName() would throw AbsentInformationException. - sourceName = location.sourceName(); - relativeSourcePath = location.sourcePath(); - } catch (AbsentInformationException e) { - String enclosingType = AdapterUtils.parseEnclosingType(fullyQualifiedName); - sourceName = enclosingType.substring(enclosingType.lastIndexOf('.') + 1) + ".java"; - relativeSourcePath = enclosingType.replace('.', File.separatorChar) + ".java"; - } - - return convertDebuggerSourceToClient(fullyQualifiedName, sourceName, relativeSourcePath, context); + return new Types.StackFrame(frameId, methodName, clientSource, clientLineNumber, context.isClientColumnsStartAt1() ? 1 : 0, presentationHint); } /** @@ -164,20 +219,42 @@ public static Types.Source convertDebuggerSourceToClient(String fullyQualifiedNa } } - private String formatMethodName(Method method, boolean showContextClass, boolean showParameter) { + private String formatMethodName(String methodName, List argumentTypeNames, String fqn, boolean showContextClass, boolean showParameter) { StringBuilder formattedName = new StringBuilder(); if (showContextClass) { - String fullyQualifiedClassName = method.declaringType().name(); - formattedName.append(SimpleTypeFormatter.trimTypeName(fullyQualifiedClassName)); + formattedName.append(SimpleTypeFormatter.trimTypeName(fqn)); formattedName.append("."); } - formattedName.append(method.name()); + formattedName.append(methodName); if (showParameter) { - List argumentTypeNames = method.argumentTypeNames().stream().map(SimpleTypeFormatter::trimTypeName).collect(Collectors.toList()); + argumentTypeNames = argumentTypeNames.stream().map(SimpleTypeFormatter::trimTypeName).collect(Collectors.toList()); formattedName.append("("); formattedName.append(String.join(",", argumentTypeNames)); formattedName.append(")"); } return formattedName.toString(); } + + static class StackFrameInfo { + public StackFrame frame; + public Location location; + public Method method; + public String methodName; + public List argumentTypeNames = new ArrayList<>(); + public boolean isNative = false; + public int lineNumber; + public ReferenceType declaringType = null; + public String typeName; + public String typeSignature; + public String sourceName = ""; + public String sourcePath = ""; + + // variables + public List visibleVariables = null; + public ObjectReference thisObject; + + public StackFrameInfo(StackFrame frame) { + this.frame = frame; + } + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StepRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StepRequestHandler.java index 72d14eb5d..4f58c113f 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StepRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StepRequestHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017-2020 Microsoft Corporation and others. + * Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -11,12 +11,15 @@ package com.microsoft.java.debug.core.adapter.handler; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import org.apache.commons.lang3.ArrayUtils; +import com.microsoft.java.debug.core.AsyncJdwpUtils; import com.microsoft.java.debug.core.DebugEvent; import com.microsoft.java.debug.core.DebugUtility; import com.microsoft.java.debug.core.IDebugSession; @@ -68,16 +71,18 @@ public CompletableFuture handle(Command command, Arguments arguments, } long threadId = ((StepArguments) arguments).threadId; - ThreadReference thread = DebugUtility.getThread(context.getDebugSession(), threadId); + ThreadReference thread = context.getThreadCache().getThread(threadId); + if (thread == null) { + thread = DebugUtility.getThread(context.getDebugSession(), threadId); + } if (thread != null) { JdiExceptionReference exception = context.getExceptionManager().removeException(threadId); context.getStepResultManager().removeMethodResult(threadId); try { + final ThreadReference targetThread = thread; ThreadState threadState = new ThreadState(); threadState.threadId = threadId; threadState.pendingStepType = command; - threadState.stackDepth = thread.frameCount(); - threadState.stepLocation = getTopFrame(thread).location(); threadState.eventSubscription = context.getDebugSession().getEventHub().events() .filter(debugEvent -> (debugEvent.event instanceof StepEvent && debugEvent.event.request().equals(threadState.pendingStepRequest)) || (debugEvent.event instanceof MethodExitEvent && debugEvent.event.request().equals(threadState.pendingMethodExitRequest)) @@ -98,27 +103,82 @@ public CompletableFuture handle(Command command, Arguments arguments, } else { threadState.pendingStepRequest = DebugUtility.createStepOverRequest(thread, null); } - threadState.pendingStepRequest.enable(); - MethodExitRequest methodExitRequest = thread.virtualMachine().eventRequestManager().createMethodExitRequest(); - methodExitRequest.addThreadFilter(thread); - methodExitRequest.addClassFilter(threadState.stepLocation.declaringType()); - if (thread.virtualMachine().canUseInstanceFilters()) { + threadState.pendingMethodExitRequest = thread.virtualMachine().eventRequestManager().createMethodExitRequest(); + threadState.pendingMethodExitRequest.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD); + + if (context.asyncJDWP()) { + List> futures = new ArrayList<>(); + futures.add(AsyncJdwpUtils.runAsync(() -> { + try { + // JDWP Command: TR_FRAMES + threadState.topFrame = getTopFrame(targetThread); + threadState.stepLocation = threadState.topFrame.location(); + threadState.pendingMethodExitRequest.addClassFilter(threadState.stepLocation.declaringType()); + if (targetThread.virtualMachine().canUseInstanceFilters()) { + try { + // JDWP Command: SF_THIS_OBJECT + ObjectReference thisObject = threadState.topFrame.thisObject(); + if (thisObject != null) { + threadState.pendingMethodExitRequest.addInstanceFilter(thisObject); + } + } catch (Exception e) { + // ignore + } + } + } catch (IncompatibleThreadStateException e) { + throw new CompletionException(e); + } + })); + futures.add(AsyncJdwpUtils.runAsync( + // JDWP Command: OR_IS_COLLECTED + () -> threadState.pendingMethodExitRequest.addThreadFilter(targetThread) + )); + futures.add(AsyncJdwpUtils.runAsync(() -> { + try { + // JDWP Command: TR_FRAME_COUNT + threadState.stackDepth = targetThread.frameCount(); + } catch (IncompatibleThreadStateException e) { + throw new CompletionException(e); + } + })); + futures.add( + // JDWP Command: ER_SET + AsyncJdwpUtils.runAsync(() -> threadState.pendingStepRequest.enable()) + ); + try { - ObjectReference thisObject = getTopFrame(thread).thisObject(); - if (thisObject != null) { - methodExitRequest.addInstanceFilter(thisObject); + AsyncJdwpUtils.await(futures); + } catch (CompletionException ex) { + if (ex.getCause() instanceof IncompatibleThreadStateException) { + throw (IncompatibleThreadStateException) ex.getCause(); } - } catch (Exception e) { - // ignore + throw ex; } + + // JDWP Command: ER_SET + threadState.pendingMethodExitRequest.enable(); + } else { + threadState.stackDepth = targetThread.frameCount(); + threadState.topFrame = getTopFrame(targetThread); + threadState.stepLocation = threadState.topFrame.location(); + threadState.pendingMethodExitRequest.addThreadFilter(thread); + threadState.pendingMethodExitRequest.addClassFilter(threadState.stepLocation.declaringType()); + if (targetThread.virtualMachine().canUseInstanceFilters()) { + try { + ObjectReference thisObject = threadState.topFrame.thisObject(); + if (thisObject != null) { + threadState.pendingMethodExitRequest.addInstanceFilter(thisObject); + } + } catch (Exception e) { + // ignore + } + } + threadState.pendingStepRequest.enable(); + threadState.pendingMethodExitRequest.enable(); } - methodExitRequest.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD); - threadState.pendingMethodExitRequest = methodExitRequest; - methodExitRequest.enable(); DebugUtility.resumeThread(thread); - ThreadsRequestHandler.checkThreadRunningAndRecycleIds(thread, context); } catch (IncompatibleThreadStateException ex) { // Roll back the Exception info if stepping fails. @@ -136,6 +196,14 @@ public CompletableFuture handle(Command command, Arguments arguments, failureMessage, ErrorCode.STEP_FAILURE, ex); + } catch (Exception ex) { + // Roll back the Exception info if stepping fails. + context.getExceptionManager().setException(threadId, exception); + final String failureMessage = String.format("Failed to step because of the error '%s'", ex.getMessage()); + throw AdapterUtils.createCompletionException( + failureMessage, + ErrorCode.STEP_FAILURE, + ex.getCause() != null ? ex.getCause() : ex); } } @@ -280,6 +348,7 @@ class ThreadState { StepRequest pendingStepRequest = null; MethodExitRequest pendingMethodExitRequest = null; int stackDepth = -1; + StackFrame topFrame = null; Location stepLocation = null; Disposable eventSubscription = null; diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ThreadsRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ThreadsRequestHandler.java index 3e4b481d5..cedbf2ae1 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ThreadsRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ThreadsRequestHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -14,9 +14,13 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import com.microsoft.java.debug.core.AsyncJdwpUtils; import com.microsoft.java.debug.core.DebugUtility; +import com.microsoft.java.debug.core.IDebugSession; import com.microsoft.java.debug.core.adapter.AdapterUtils; import com.microsoft.java.debug.core.adapter.ErrorCode; import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; @@ -77,14 +81,14 @@ public CompletableFuture handle(Command command, Arguments arguments, private CompletableFuture threads(Requests.ThreadsArguments arguments, Response response, IDebugAdapterContext context) { ArrayList threads = new ArrayList<>(); try { - for (ThreadReference thread : context.getDebugSession().getAllThreads()) { - if (thread.isCollected()) { - continue; - } - Types.Thread clientThread = new Types.Thread(thread.uniqueID(), "Thread [" + thread.name() + "]"); - threads.add(clientThread); + List allThreads = context.getDebugSession().getAllThreads(); + context.getThreadCache().resetThreads(allThreads); + allThreads = allThreads.stream().filter((thread) -> !context.getThreadCache().isDeathThread(thread.uniqueID())).toList(); + List jdiThreads = resolveThreadInfos(allThreads, context); + for (ThreadInfo jdiThread : jdiThreads) { + threads.add(new Types.Thread(jdiThread.thread.uniqueID(), "Thread [" + jdiThread.name + "]")); } - } catch (ObjectCollectedException ex) { + } catch (ObjectCollectedException | CancellationException | CompletionException ex) { // allThreads may throw VMDisconnectedException when VM terminates and thread.name() may throw ObjectCollectedException // when the thread is exiting. } @@ -92,6 +96,33 @@ private CompletableFuture threads(Requests.ThreadsArguments arguments, return CompletableFuture.completedFuture(response); } + private static List resolveThreadInfos(List allThreads, IDebugAdapterContext context) { + List threadInfos = new ArrayList<>(allThreads.size()); + List> futures = new ArrayList<>(); + for (ThreadReference thread : allThreads) { + ThreadInfo threadInfo = new ThreadInfo(thread); + long threadId = thread.uniqueID(); + if (context.getThreadCache().getThreadName(threadId) != null) { + threadInfo.name = context.getThreadCache().getThreadName(threadId); + } else { + if (context.asyncJDWP()) { + futures.add(AsyncJdwpUtils.runAsync(() -> { + threadInfo.name = threadInfo.thread.name(); + context.getThreadCache().setThreadName(threadId, threadInfo.name); + })); + } else { + threadInfo.name = threadInfo.thread.name(); + context.getThreadCache().setThreadName(threadId, threadInfo.name); + } + } + + threadInfos.add(threadInfo); + } + + AsyncJdwpUtils.await(futures); + return threadInfos; + } + private CompletableFuture pause(Requests.PauseArguments arguments, Response response, IDebugAdapterContext context) { ThreadReference thread = DebugUtility.getThread(context.getDebugSession(), arguments.threadId); if (thread != null) { @@ -108,7 +139,10 @@ private CompletableFuture pause(Requests.PauseArguments arguments, Res private CompletableFuture resume(Requests.ContinueArguments arguments, Response response, IDebugAdapterContext context) { boolean allThreadsContinued = true; - ThreadReference thread = DebugUtility.getThread(context.getDebugSession(), arguments.threadId); + ThreadReference thread = context.getThreadCache().getThread(arguments.threadId); + if (thread == null) { + thread = DebugUtility.getThread(context.getDebugSession(), arguments.threadId); + } /** * See the jdi doc https://docs.oracle.com/javase/7/docs/jdk/api/jpda/jdi/com/sun/jdi/ThreadReference.html#resume(), * suspends of both the virtual machine and individual threads are counted. Before a thread will run again, it must @@ -123,7 +157,11 @@ private CompletableFuture resume(Requests.ContinueArguments arguments, } else { context.getStepResultManager().removeAllMethodResults(); context.getExceptionManager().removeAllExceptions(); - context.getDebugSession().resume(); + if (context.asyncJDWP()) { + resumeVMAsync(context.getDebugSession()); + } else { + context.getDebugSession().resume(); + } context.getRecyclableIdPool().removeAllObjects(); } response.body = new Responses.ContinueResponseBody(allThreadsContinued); @@ -132,7 +170,11 @@ private CompletableFuture resume(Requests.ContinueArguments arguments, private CompletableFuture resumeAll(Requests.ThreadOperationArguments arguments, Response response, IDebugAdapterContext context) { context.getExceptionManager().removeAllExceptions(); - context.getDebugSession().resume(); + if (context.asyncJDWP()) { + resumeVMAsync(context.getDebugSession()); + } else { + context.getDebugSession().resume(); + } context.getProtocolServer().sendEvent(new Events.ContinuedEvent(arguments.threadId, true)); context.getRecyclableIdPool().removeAllObjects(); return CompletableFuture.completedFuture(response); @@ -140,16 +182,19 @@ private CompletableFuture resumeAll(Requests.ThreadOperationArguments private CompletableFuture resumeOthers(Requests.ThreadOperationArguments arguments, Response response, IDebugAdapterContext context) { List threads = DebugUtility.getAllThreadsSafely(context.getDebugSession()); + List> futures = new ArrayList<>(); for (ThreadReference thread : threads) { - long threadId = thread.uniqueID(); - if (threadId != arguments.threadId && thread.isSuspended()) { - context.getExceptionManager().removeException(threadId); - DebugUtility.resumeThread(thread); - context.getProtocolServer().sendEvent(new Events.ContinuedEvent(threadId)); - checkThreadRunningAndRecycleIds(thread, context); + if (thread.uniqueID() == arguments.threadId) { + continue; } - } + if (context.asyncJDWP()) { + futures.add(AsyncJdwpUtils.runAsync(() -> resumeThread(thread, context))); + } else { + resumeThread(thread, context); + } + } + AsyncJdwpUtils.await(futures); return CompletableFuture.completedFuture(response); } @@ -161,14 +206,19 @@ private CompletableFuture pauseAll(Requests.ThreadOperationArguments a private CompletableFuture pauseOthers(Requests.ThreadOperationArguments arguments, Response response, IDebugAdapterContext context) { List threads = DebugUtility.getAllThreadsSafely(context.getDebugSession()); + List> futures = new ArrayList<>(); for (ThreadReference thread : threads) { - long threadId = thread.uniqueID(); - if (threadId != arguments.threadId && !thread.isCollected() && !thread.isSuspended()) { - thread.suspend(); - context.getProtocolServer().sendEvent(new Events.StoppedEvent("pause", threadId)); + if (thread.uniqueID() == arguments.threadId) { + continue; } - } + if (context.asyncJDWP()) { + futures.add(AsyncJdwpUtils.runAsync(() -> pauseThread(thread, context))); + } else { + pauseThread(thread, context); + } + } + AsyncJdwpUtils.await(futures); return CompletableFuture.completedFuture(response); } @@ -179,13 +229,7 @@ public static void checkThreadRunningAndRecycleIds(ThreadReference thread, IDebu try { IEvaluationProvider engine = context.getProvider(IEvaluationProvider.class); engine.clearState(thread); - boolean allThreadsRunning = !DebugUtility.getAllThreadsSafely(context.getDebugSession()).stream() - .anyMatch(ThreadReference::isSuspended); - if (allThreadsRunning) { - context.getRecyclableIdPool().removeAllObjects(); - } else { - context.getRecyclableIdPool().removeObjectsByOwner(thread.uniqueID()); - } + context.getRecyclableIdPool().removeObjectsByOwner(thread.uniqueID()); } catch (VMDisconnectedException ex) { // isSuspended may throw VMDisconnectedException when the VM terminates context.getRecyclableIdPool().removeAllObjects(); @@ -194,4 +238,58 @@ public static void checkThreadRunningAndRecycleIds(ThreadReference thread, IDebu context.getRecyclableIdPool().removeObjectsByOwner(thread.uniqueID()); } } + + private void resumeVMAsync(IDebugSession debugSession) { + List> futures = new ArrayList<>(); + for (ThreadReference tr : DebugUtility.getAllThreadsSafely(debugSession)) { + futures.add(AsyncJdwpUtils.runAsync(() -> { + try { + while (tr.suspendCount() > 1) { + tr.resume(); + } + } catch (ObjectCollectedException ex) { + // Ignore it if the thread is garbage collected. + } + })); + } + + AsyncJdwpUtils.await(futures); + debugSession.getVM().resume(); + } + + private void resumeThread(ThreadReference thread, IDebugAdapterContext context) { + try { + int suspends = thread.suspendCount(); + if (suspends > 0) { + long threadId = thread.uniqueID(); + context.getExceptionManager().removeException(threadId); + DebugUtility.resumeThread(thread, suspends); + context.getProtocolServer().sendEvent(new Events.ContinuedEvent(threadId)); + checkThreadRunningAndRecycleIds(thread, context); + } + } catch (ObjectCollectedException ex) { + // ignore it. + } + } + + private void pauseThread(ThreadReference thread, IDebugAdapterContext context) { + try { + if (!thread.isSuspended()) { + long threadId = thread.uniqueID(); + thread.suspend(); + context.getProtocolServer().sendEvent(new Events.StoppedEvent("pause", threadId)); + } + } catch (ObjectCollectedException ex) { + // ignore it if the thread is garbage collected. + } + } + + static class ThreadInfo { + public ThreadReference thread; + public String name; + + public ThreadInfo(ThreadReference thread) { + this.thread = thread; + } + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/VariablesRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/VariablesRequestHandler.java index 8f37ef574..76bb5a4db 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/VariablesRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/VariablesRequestHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017-2021 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -19,11 +19,14 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; +import com.microsoft.java.debug.core.AsyncJdwpUtils; import com.microsoft.java.debug.core.Configuration; import com.microsoft.java.debug.core.DebugSettings; import com.microsoft.java.debug.core.JdiMethodResult; @@ -39,6 +42,7 @@ import com.microsoft.java.debug.core.adapter.variables.JavaLogicalStructure.LogicalVariable; import com.microsoft.java.debug.core.adapter.variables.JavaLogicalStructureManager; import com.microsoft.java.debug.core.adapter.variables.StackFrameReference; +import com.microsoft.java.debug.core.adapter.variables.StringReferenceProxy; import com.microsoft.java.debug.core.adapter.variables.Variable; import com.microsoft.java.debug.core.adapter.variables.VariableDetailUtils; import com.microsoft.java.debug.core.adapter.variables.VariableProxy; @@ -57,6 +61,7 @@ import com.sun.jdi.InvalidStackFrameException; import com.sun.jdi.ObjectReference; import com.sun.jdi.StackFrame; +import com.sun.jdi.StringReference; import com.sun.jdi.Type; import com.sun.jdi.Value; @@ -96,7 +101,7 @@ public CompletableFuture handle(Command command, Arguments arguments, VariableProxy containerNode = (VariableProxy) container; - if (containerNode.isLazyVariable() && DebugSettings.getCurrent().showToString) { + if (supportsToStringView(context) && containerNode.isLazyVariable()) { Types.Variable typedVariable = this.resolveLazyVariable(context, containerNode, variableFormatter, options, evaluationEngine); if (typedVariable != null) { list.add(typedVariable); @@ -123,24 +128,29 @@ public CompletableFuture handle(Command command, Arguments arguments, String returnIcon = (AdapterUtils.isWin || AdapterUtils.isMac) ? "⎯►" : "->"; childrenList.add(new Variable(returnIcon + result.method.name() + "()", result.value, null)); } - childrenList.addAll(VariableUtils.listLocalVariables(frame)); - Variable thisVariable = VariableUtils.getThisVariable(frame); - if (thisVariable != null) { - childrenList.add(thisVariable); - } - if (showStaticVariables && frame.location().method().isStatic()) { - childrenList.addAll(VariableUtils.listStaticVariables(frame)); + + if (context.asyncJDWP()) { + childrenList.addAll(getVariablesOfFrameAsync(frame, showStaticVariables)); + } else { + childrenList.addAll(VariableUtils.listLocalVariables(frame)); + Variable thisVariable = VariableUtils.getThisVariable(frame); + if (thisVariable != null) { + childrenList.add(thisVariable); + } + if (showStaticVariables && frame.location().method().isStatic()) { + childrenList.addAll(VariableUtils.listStaticVariables(frame)); + } } - } catch (AbsentInformationException | InternalException | InvalidStackFrameException e) { + } catch (CompletionException | InternalException | InvalidStackFrameException | CancellationException | AbsentInformationException e) { throw AdapterUtils.createCompletionException( String.format("Failed to get variables. Reason: %s", e.toString()), ErrorCode.GET_VARIABLE_FAILURE, - e); + e.getCause() != null ? e.getCause() : e); } } else { try { ObjectReference containerObj = (ObjectReference) containerNode.getProxiedVariable(); - if (DebugSettings.getCurrent().showLogicalStructure && evaluationEngine != null) { + if (supportsLogicStructureView(context) && evaluationEngine != null) { JavaLogicalStructure logicalStructure = null; try { logicalStructure = JavaLogicalStructureManager.getLogicalStructure(containerObj); @@ -232,6 +242,17 @@ public CompletableFuture handle(Command command, Arguments arguments, } }); } + + // Since JDI caches the fetched properties locally, in async mode we can warm up the JDI cache in advance. + if (context.asyncJDWP()) { + try { + AsyncJdwpUtils.await(warmUpJDICache(childrenList)); + } catch (CompletionException | CancellationException e) { + response.body = new Responses.VariablesResponseBody(list); + return CompletableFuture.completedFuture(response); + } + } + for (Variable javaVariable : childrenList) { Value value = javaVariable.value; String name = javaVariable.name; @@ -242,7 +263,7 @@ public CompletableFuture handle(Command command, Arguments arguments, Value sizeValue = null; if (value instanceof ArrayReference) { indexedVariables = ((ArrayReference) value).length(); - } else if (value instanceof ObjectReference && DebugSettings.getCurrent().showLogicalStructure && evaluationEngine != null) { + } else if (supportsLogicStructureView(context) && value instanceof ObjectReference && evaluationEngine != null) { try { JavaLogicalStructure structure = JavaLogicalStructureManager.getLogicalStructure((ObjectReference) value); if (structure != null && structure.getSizeExpression() != null) { @@ -310,7 +331,7 @@ public CompletableFuture handle(Command command, Arguments arguments, // If failed to resolve the variable value, skip the details info as well. } else if (sizeValue != null) { detailsValue = "size=" + variableFormatter.valueToString(sizeValue, options); - } else if (DebugSettings.getCurrent().showToString) { + } else if (supportsToStringView(context)) { if (VariableDetailUtils.isLazyLoadingSupported(value) && varProxy != null) { varProxy.setLazyVariable(true); } else { @@ -352,6 +373,14 @@ public CompletableFuture handle(Command command, Arguments arguments, return CompletableFuture.completedFuture(response); } + private boolean supportsLogicStructureView(IDebugAdapterContext context) { + return (!context.asyncJDWP() || context.isLocalDebugging()) && DebugSettings.getCurrent().showLogicalStructure; + } + + private boolean supportsToStringView(IDebugAdapterContext context) { + return (!context.asyncJDWP() || context.isLocalDebugging()) && DebugSettings.getCurrent().showToString; + } + private Types.Variable resolveLazyVariable(IDebugAdapterContext context, VariableProxy containerNode, IVariableFormatter variableFormatter, Map options, IEvaluationProvider evaluationEngine) { VariableProxy valueReferenceProxy = new VariableProxy(containerNode.getThread(), containerNode.getScope(), @@ -384,4 +413,57 @@ private Set getDuplicateNames(Collection list) { } return result; } + + private List getVariablesOfFrameAsync(StackFrame frame, boolean showStaticVariables) { + CompletableFuture> localVariables = VariableUtils.listLocalVariablesAsync(frame); + CompletableFuture thisVariable = VariableUtils.getThisVariableAsync(frame); + CompletableFuture>[] staticVariables = new CompletableFuture[1]; + if (showStaticVariables && frame.location().method().isStatic()) { + staticVariables[0] = VariableUtils.listStaticVariablesAsync(frame); + } + + CompletableFuture futures = staticVariables[0] == null ? CompletableFuture.allOf(localVariables, thisVariable) + : CompletableFuture.allOf(localVariables, thisVariable, staticVariables[0]); + + AsyncJdwpUtils.await(futures); + + List result = new ArrayList<>(); + result.addAll(localVariables.join()); + Variable thisVar = thisVariable.join(); + if (thisVar != null) { + result.add(thisVar); + } + + if (staticVariables[0] != null) { + result.addAll(staticVariables[0].join()); + } + + return result; + } + + private CompletableFuture warmUpJDICache(List variables) { + List> fetchVariableInfoFutures = new ArrayList<>(); + for (Variable javaVariable : variables) { + Value value = javaVariable.value; + if (value instanceof ArrayReference) { + // JDWP Command: AR_LENGTH + fetchVariableInfoFutures.add(AsyncJdwpUtils.runAsync(() -> ((ArrayReference) value).length())); + } else if (value instanceof StringReference) { + // JDWP Command: SR_VALUE + fetchVariableInfoFutures.add(AsyncJdwpUtils.runAsync(() -> { + String strValue = ((StringReference) value).value(); + javaVariable.value = new StringReferenceProxy((StringReference) value, strValue); + })); + } + + if (value instanceof ObjectReference) { + // JDWP Command: OR_REFERENCE_TYPE, RT_SIGNATURE + fetchVariableInfoFutures.add(AsyncJdwpUtils.runAsync(() -> { + value.type().signature(); + })); + } + } + + return CompletableFuture.allOf(fetchVariableInfoFutures.toArray(new CompletableFuture[0])); + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/variables/StringReferenceProxy.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/variables/StringReferenceProxy.java new file mode 100644 index 000000000..82c4da56e --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/variables/StringReferenceProxy.java @@ -0,0 +1,121 @@ +/******************************************************************************* + * Copyright (c) 2022 Microsoft Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Microsoft Corporation - initial API and implementation + *******************************************************************************/ + +package com.microsoft.java.debug.core.adapter.variables; + +import java.util.List; +import java.util.Map; + +import com.sun.jdi.ClassNotLoadedException; +import com.sun.jdi.Field; +import com.sun.jdi.IncompatibleThreadStateException; +import com.sun.jdi.InvalidTypeException; +import com.sun.jdi.InvocationException; +import com.sun.jdi.Method; +import com.sun.jdi.ObjectReference; +import com.sun.jdi.ReferenceType; +import com.sun.jdi.StringReference; +import com.sun.jdi.ThreadReference; +import com.sun.jdi.Type; +import com.sun.jdi.Value; +import com.sun.jdi.VirtualMachine; + +public class StringReferenceProxy implements StringReference { + private StringReference delegateStringRef; + private String value = null; + + public StringReferenceProxy(StringReference sr, String value) { + this.delegateStringRef = sr; + this.value = value; + } + + public String value() { + if (value != null) { + return value; + } + + return delegateStringRef.value(); + } + + public ReferenceType referenceType() { + return delegateStringRef.referenceType(); + } + + public VirtualMachine virtualMachine() { + return delegateStringRef.virtualMachine(); + } + + public String toString() { + return delegateStringRef.toString(); + } + + public Value getValue(Field sig) { + return delegateStringRef.getValue(sig); + } + + public Map getValues(List fields) { + return delegateStringRef.getValues(fields); + } + + public void setValue(Field field, Value value) throws InvalidTypeException, ClassNotLoadedException { + delegateStringRef.setValue(field, value); + } + + public Value invokeMethod(ThreadReference thread, Method method, List arguments, int options) + throws InvalidTypeException, ClassNotLoadedException, IncompatibleThreadStateException, + InvocationException { + return delegateStringRef.invokeMethod(thread, method, arguments, options); + } + + public Type type() { + return delegateStringRef.type(); + } + + public void disableCollection() { + delegateStringRef.disableCollection(); + } + + public void enableCollection() { + delegateStringRef.enableCollection(); + } + + public boolean isCollected() { + return delegateStringRef.isCollected(); + } + + public long uniqueID() { + return delegateStringRef.uniqueID(); + } + + public List waitingThreads() throws IncompatibleThreadStateException { + return delegateStringRef.waitingThreads(); + } + + public ThreadReference owningThread() throws IncompatibleThreadStateException { + return delegateStringRef.owningThread(); + } + + public int entryCount() throws IncompatibleThreadStateException { + return delegateStringRef.entryCount(); + } + + public List referringObjects(long maxReferrers) { + return delegateStringRef.referringObjects(maxReferrers); + } + + public boolean equals(Object obj) { + return delegateStringRef.equals(obj); + } + + public int hashCode() { + return delegateStringRef.hashCode(); + } +} diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/variables/VariableUtils.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/variables/VariableUtils.java index d53e2705c..bd8c8036d 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/variables/VariableUtils.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/variables/VariableUtils.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017-2020 Microsoft Corporation and others. + * Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -15,11 +15,15 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.function.Consumer; +import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; +import com.microsoft.java.debug.core.AsyncJdwpUtils; import com.microsoft.java.debug.core.Configuration; import com.microsoft.java.debug.core.DebugSettings; import com.microsoft.java.debug.core.adapter.formatter.NumericFormatEnum; @@ -213,6 +217,91 @@ public static List listLocalVariables(StackFrame stackFrame) throws Ab return res; } + public static CompletableFuture> listLocalVariablesAsync(StackFrame stackFrame) { + CompletableFuture> future = new CompletableFuture<>(); + if (stackFrame.location().method().isNative()) { + return CompletableFuture.completedFuture(new ArrayList<>()); + } + + AsyncJdwpUtils.supplyAsync(() -> { + try { + return stackFrame.visibleVariables(); + } catch (AbsentInformationException ex) { + throw new CompletionException(ex); + } + }).thenCompose((visibleVariables) -> { + // When using the API StackFrame.getValues() to batch fetch the variable values, the JDI + // probably throws timeout exception if the variables to be passed at one time are large. + // So use paging to fetch the values in chunks. + return bulkFetchValuesAsync(visibleVariables, DebugSettings.getCurrent().limitOfVariablesPerJdwpRequest, (currentPage) -> { + Map values = stackFrame.getValues(currentPage); + List result = new ArrayList<>(); + for (LocalVariable localVariable : currentPage) { + Variable var = new Variable(localVariable.name(), values.get(localVariable)); + var.local = localVariable; + result.add(var); + } + + return result; + }); + }).whenComplete((res, ex) -> { + if (ex instanceof CompletionException && ex.getCause() != null) { + ex = ex.getCause(); + } + + if (ex instanceof AbsentInformationException) { + // avoid listing variable on native methods + try { + if (stackFrame.location().method().argumentTypes().size() == 0) { + future.complete(new ArrayList<>()); + return; + } + } catch (ClassNotLoadedException ex2) { + // ignore since the method is hit. + } + // 1. in oracle implementations, when there is no debug information, the AbsentInformationException will be + // thrown, then we need to retrieve arguments from stackFrame#getArgumentValues. + // 2. in eclipse jdt implementations, when there is no debug information, stackFrame#visibleVariables will + // return some generated variables like arg0, arg1, and the stackFrame#getArgumentValues will return null + + // for both scenarios, we need to handle the possible null returned by stackFrame#getArgumentValues and + // we need to call stackFrame.getArgumentValues get the arguments if AbsentInformationException is thrown + int argId = 0; + try { + List arguments = stackFrame.getArgumentValues(); + if (arguments == null) { + future.complete(new ArrayList<>()); + return; + } + + List variables = new ArrayList<>(); + for (Value argValue : arguments) { + Variable var = new Variable("arg" + argId, argValue); + var.argumentIndex = argId++; + variables.add(var); + } + future.complete(variables); + } catch (InternalException ex2) { + // From Oracle's forums: + // This could be a JPDA bug. Unexpected JDWP Error: 32 means that an 'opaque' frame was + // detected at the lower JPDA levels, + // typically a native frame. + if (ex2.errorCode() != 32) { + throw ex2; + } + } + } else if (ex != null) { + future.complete(new ArrayList<>()); + } else { + future.complete(res.stream() + .flatMap(List::stream) + .collect(Collectors.toList())); + } + }); + + return future; + } + /** * Get the this variable of an stack frame. * @@ -228,6 +317,16 @@ public static Variable getThisVariable(StackFrame stackFrame) { return new Variable("this", thisObject); } + public static CompletableFuture getThisVariableAsync(StackFrame stackFrame) { + return AsyncJdwpUtils.supplyAsync(() -> { + ObjectReference thisObject = stackFrame.thisObject(); + if (thisObject == null) { + return null; + } + return new Variable("this", thisObject); + }); + } + /** * Get the static variable of an stack frame. * @@ -251,6 +350,40 @@ public static List listStaticVariables(StackFrame stackFrame) { return res; } + public static CompletableFuture> listStaticVariablesAsync(StackFrame stackFrame) { + CompletableFuture> future = new CompletableFuture<>(); + ReferenceType type = stackFrame.location().declaringType(); + AsyncJdwpUtils.supplyAsync(() -> { + return type.allFields().stream().filter(TypeComponent::isStatic).collect(Collectors.toList()); + }).thenCompose((fields) -> { + return bulkFetchValuesAsync(fields, DebugSettings.getCurrent().limitOfVariablesPerJdwpRequest, (currentPage) -> { + List variables = new ArrayList<>(); + Map fieldValues = type.getValues(currentPage); + for (Field currentField : currentPage) { + Variable var = new Variable(currentField.name(), fieldValues.get(currentField)); + var.field = currentField; + variables.add(var); + } + + return variables; + }); + }).whenComplete((res, ex) -> { + if (ex instanceof CompletionException && ex.getCause() != null) { + ex = ex.getCause(); + } + + if (ex != null) { + future.complete(new ArrayList<>()); + } else { + future.complete(res.stream() + .flatMap(List::stream) + .collect(Collectors.toList())); + } + }); + + return future; + } + /** * Apply the display options for variable formatter, it is used in variable and evaluate requests, controls the display content in * variable view/debug console. @@ -316,6 +449,23 @@ private static void bulkFetchValues(List elements, int numberPerPage, Con } } + private static CompletableFuture> bulkFetchValuesAsync(List elements, int numberPerPage, Function, R> function) { + int size = elements.size(); + numberPerPage = numberPerPage < 1 ? 1 : numberPerPage; + int page = size / numberPerPage + Math.min(size % numberPerPage, 1); + List> futures = new ArrayList<>(); + for (int i = 0; i < page; i++) { + int pageStart = i * numberPerPage; + int pageEnd = Math.min(pageStart + numberPerPage, size); + final List currentPage = elements.subList(pageStart, pageEnd); + futures.add(AsyncJdwpUtils.supplyAsync(() -> { + return function.apply(currentPage); + })); + } + + return AsyncJdwpUtils.all(futures); + } + private VariableUtils() { }