-
Notifications
You must be signed in to change notification settings - Fork 81
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Jedi cleanup and fixes #3174
base: main
Are you sure you want to change the base?
Jedi cleanup and fixes #3174
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,13 +24,15 @@ | |
import io.deephaven.proto.backplane.grpc.TypedTicket; | ||
import io.deephaven.proto.backplane.script.grpc.*; | ||
import io.deephaven.server.console.completer.JavaAutoCompleteObserver; | ||
import io.deephaven.server.console.completer.JediCompleter; | ||
import io.deephaven.server.console.completer.PythonAutoCompleteObserver; | ||
import io.deephaven.server.session.SessionCloseableObserver; | ||
import io.deephaven.server.session.SessionService; | ||
import io.deephaven.server.session.SessionState; | ||
import io.deephaven.server.session.SessionState.ExportBuilder; | ||
import io.deephaven.server.session.TicketRouter; | ||
import io.grpc.stub.StreamObserver; | ||
import org.jpy.PyModule; | ||
import org.jpy.PyObject; | ||
|
||
import javax.inject.Inject; | ||
|
@@ -109,14 +111,13 @@ public void startConsole(StartConsoleRequest request, StreamObserver<StartConsol | |
.onError(responseObserver) | ||
.submit(() -> { | ||
final ScriptSession scriptSession = new DelegatingScriptSession(scriptSessionProvider.get()); | ||
|
||
final StartConsoleResponse response = StartConsoleResponse.newBuilder() | ||
.setResultId(request.getResultId()) | ||
.build(); | ||
safelyExecute(() -> { | ||
responseObserver.onNext(StartConsoleResponse.newBuilder() | ||
.setResultId(request.getResultId()) | ||
.build()); | ||
responseObserver.onNext(response); | ||
responseObserver.onCompleted(); | ||
}); | ||
|
||
return scriptSession; | ||
}); | ||
}); | ||
|
@@ -255,25 +256,29 @@ public StreamObserver<AutoCompleteRequest> autoCompleteStream( | |
if (AUTOCOMPLETE_DISABLED) { | ||
return new NoopAutoCompleteObserver(session, responseObserver); | ||
} | ||
if (PythonDeephavenSession.SCRIPT_TYPE.equals(scriptSessionProvider.get().scriptType())) { | ||
PyObject[] settings = new PyObject[1]; | ||
safelyExecute(() -> { | ||
final ScriptSession scriptSession = scriptSessionProvider.get(); | ||
scriptSession.evaluateScript( | ||
"from deephaven_internal.auto_completer import jedi_settings ; jedi_settings.set_scope(globals())"); | ||
settings[0] = (PyObject) scriptSession.getVariable("jedi_settings"); | ||
}); | ||
boolean canJedi = settings[0] != null && settings[0].call("can_jedi").getBooleanValue(); | ||
log.info().append(canJedi ? "Using jedi for python autocomplete" | ||
: "No jedi dependency available in python environment; disabling autocomplete.").endl(); | ||
return canJedi ? new PythonAutoCompleteObserver(responseObserver, scriptSessionProvider, session) | ||
: new NoopAutoCompleteObserver(session, responseObserver); | ||
final ScriptSession scriptSession = scriptSessionProvider.get(); | ||
if (PythonDeephavenSession.SCRIPT_TYPE.equals(scriptSession.scriptType())) { | ||
final PyObject scope = ((PythonDeephavenSession) scriptSession).scope().mainGlobals().unwrap(); | ||
// noinspection EmptyTryBlock,unused | ||
try (final JediCompleter _unused = jedi(scope)) { | ||
// ensure we can create it | ||
} catch (RuntimeException e) { | ||
log.debug(e).append("Unable to create autocomplete; is jedi installed?").endl(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will anybody ever see this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if we raise the log level so it's easily noticed, we can include a message about how to disable autocomplete altogether. |
||
return new NoopAutoCompleteObserver(session, responseObserver); | ||
} | ||
log.debug().append("Using jedi for python autocomplete").endl(); | ||
return new PythonAutoCompleteObserver(responseObserver, session, () -> jedi(scope)); | ||
} | ||
|
||
return new JavaAutoCompleteObserver(session, responseObserver); | ||
}); | ||
} | ||
|
||
private static JediCompleter jedi(final PyObject scope) { | ||
try (final PyModule pyModule = PyModule.importModule("deephaven_internal.auto_completer")) { | ||
return pyModule.call("Completer", scope).createProxy(JediCompleter.class); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. a code comment here would be nice |
||
} | ||
} | ||
|
||
private static class NoopAutoCompleteObserver extends SessionCloseableObserver<AutoCompleteResponse> | ||
implements StreamObserver<AutoCompleteRequest> { | ||
public NoopAutoCompleteObserver(SessionState session, StreamObserver<AutoCompleteResponse> responseObserver) { | ||
|
@@ -284,13 +289,12 @@ public NoopAutoCompleteObserver(SessionState session, StreamObserver<AutoComplet | |
public void onNext(AutoCompleteRequest value) { | ||
// This implementation only responds to autocomplete requests with "success, nothing found" | ||
if (value.getRequestCase() == AutoCompleteRequest.RequestCase.GET_COMPLETION_ITEMS) { | ||
safelyExecuteLocked(responseObserver, () -> { | ||
responseObserver.onNext(AutoCompleteResponse.newBuilder() | ||
.setCompletionItems( | ||
GetCompletionItemsResponse.newBuilder().setSuccess(true) | ||
.setRequestId(value.getGetCompletionItems().getRequestId())) | ||
.build()); | ||
}); | ||
final AutoCompleteResponse response = AutoCompleteResponse.newBuilder() | ||
.setCompletionItems(GetCompletionItemsResponse.newBuilder() | ||
.setSuccess(true) | ||
.setRequestId(value.getGetCompletionItems().getRequestId())) | ||
.build(); | ||
safelyExecuteLocked(responseObserver, () -> responseObserver.onNext(response)); | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package io.deephaven.server.console.completer; | ||
|
||
import org.jpy.PyObject; | ||
|
||
import java.io.Closeable; | ||
|
||
public interface JediCompleter extends Closeable { | ||
|
||
void open_doc(String text, String uri, int version); | ||
|
||
String get_doc(String uri); | ||
|
||
void update_doc(String document, String uri, int version); | ||
|
||
void close_doc(String uri); | ||
|
||
PyObject do_completion(String uri, int version, int line, int character); | ||
|
||
@Override | ||
void close(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,17 +7,33 @@ | |
import io.deephaven.io.logger.Logger; | ||
import io.deephaven.lang.completion.ChunkerCompleter; | ||
import io.deephaven.lang.parse.CompletionParser; | ||
import io.deephaven.proto.backplane.script.grpc.*; | ||
import io.deephaven.proto.backplane.script.grpc.AutoCompleteRequest; | ||
import io.deephaven.proto.backplane.script.grpc.AutoCompleteResponse; | ||
import io.deephaven.proto.backplane.script.grpc.ChangeDocumentRequest; | ||
import io.deephaven.proto.backplane.script.grpc.CloseDocumentRequest; | ||
import io.deephaven.proto.backplane.script.grpc.CompletionItem; | ||
import io.deephaven.proto.backplane.script.grpc.DocumentRange; | ||
import io.deephaven.proto.backplane.script.grpc.GetCompletionItemsRequest; | ||
import io.deephaven.proto.backplane.script.grpc.GetCompletionItemsResponse; | ||
import io.deephaven.proto.backplane.script.grpc.OpenDocumentRequest; | ||
import io.deephaven.proto.backplane.script.grpc.Position; | ||
import io.deephaven.proto.backplane.script.grpc.TextDocumentItem; | ||
import io.deephaven.proto.backplane.script.grpc.TextEdit; | ||
import io.deephaven.proto.backplane.script.grpc.VersionedTextDocumentIdentifier; | ||
import io.deephaven.server.console.ConsoleServiceGrpcImpl; | ||
import io.deephaven.server.session.SessionCloseableObserver; | ||
import io.deephaven.server.session.SessionState; | ||
import io.deephaven.util.SafeCloseable; | ||
import io.grpc.stub.StreamObserver; | ||
import org.jpy.PyObject; | ||
|
||
import javax.inject.Provider; | ||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.WeakHashMap; | ||
import java.util.function.Function; | ||
import java.util.function.Supplier; | ||
|
||
import static io.deephaven.extensions.barrage.util.GrpcUtil.safelyExecuteLocked; | ||
|
||
|
@@ -33,12 +49,29 @@ public class PythonAutoCompleteObserver extends SessionCloseableObserver<AutoCom | |
* We only log timing for completions that take longer than, currently, 100ms | ||
*/ | ||
private static final long HUNDRED_MS_IN_NS = 100_000_000; | ||
private final Provider<ScriptSession> scriptSession; | ||
|
||
public PythonAutoCompleteObserver(StreamObserver<AutoCompleteResponse> responseObserver, | ||
Provider<ScriptSession> scriptSession, final SessionState session) { | ||
/** Track parsers by their session state, to ensure each session has its own, singleton, parser */ | ||
private static final Map<SessionState, JediCompleter> parsers = Collections.synchronizedMap(new WeakHashMap<>()); | ||
Comment on lines
+53
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. extremely unsafe to do this. jedi is NOT threadsafe and should NOT be accessed by more than one caller at a time. |
||
|
||
private JediCompleter ensureParserForSession(SessionState session, Supplier<JediCompleter> supplier) { | ||
return parsers.computeIfAbsent(session, s -> { | ||
final JediCompleter jedi = supplier.get(); | ||
s.addOnCloseCallback(() -> { | ||
parsers.remove(s); | ||
jedi.close(); | ||
}); | ||
return jedi; | ||
}); | ||
} | ||
|
||
private final JediCompleter jedi; | ||
|
||
public PythonAutoCompleteObserver( | ||
StreamObserver<AutoCompleteResponse> responseObserver, | ||
SessionState session, | ||
Supplier<JediCompleter> supplier) { | ||
super(session, responseObserver); | ||
this.scriptSession = scriptSession; | ||
this.jedi = ensureParserForSession(session, supplier); | ||
} | ||
|
||
@Override | ||
|
@@ -48,27 +81,22 @@ public void onNext(AutoCompleteRequest value) { | |
case OPEN_DOCUMENT: { | ||
final OpenDocumentRequest openDoc = value.getOpenDocument(); | ||
final TextDocumentItem doc = openDoc.getTextDocument(); | ||
PyObject completer = (PyObject) scriptSession.get().getVariable("jedi_settings"); | ||
completer.callMethod("open_doc", doc.getText(), doc.getUri(), doc.getVersion()); | ||
jedi.open_doc(doc.getText(), doc.getUri(), doc.getVersion()); | ||
break; | ||
} | ||
case CHANGE_DOCUMENT: { | ||
ChangeDocumentRequest request = value.getChangeDocument(); | ||
final VersionedTextDocumentIdentifier text = request.getTextDocument(); | ||
|
||
PyObject completer = (PyObject) scriptSession.get().getVariable("jedi_settings"); | ||
String uri = text.getUri(); | ||
int version = text.getVersion(); | ||
String document = completer.callMethod("get_doc", text.getUri()).getStringValue(); | ||
|
||
String document = jedi.get_doc(text.getUri()); | ||
final List<ChangeDocumentRequest.TextDocumentContentChangeEvent> changes = | ||
request.getContentChangesList(); | ||
document = CompletionParser.updateDocumentChanges(uri, version, document, changes); | ||
if (document == null) { | ||
return; | ||
} | ||
|
||
completer.callMethod("update_doc", document, uri, version); | ||
jedi.update_doc(document, uri, version); | ||
break; | ||
} | ||
case GET_COMPLETION_ITEMS: { | ||
|
@@ -85,13 +113,11 @@ public void onNext(AutoCompleteRequest value) { | |
} | ||
case CLOSE_DOCUMENT: { | ||
CloseDocumentRequest request = value.getCloseDocument(); | ||
PyObject completer = (PyObject) scriptSession.get().getVariable("jedi_settings"); | ||
completer.callMethod("close_doc", request.getTextDocument().getUri()); | ||
jedi.close_doc(request.getTextDocument().getUri()); | ||
break; | ||
} | ||
case REQUEST_NOT_SET: { | ||
throw GrpcUtil.statusRuntimeException(Code.INVALID_ARGUMENT, | ||
"Autocomplete command missing request"); | ||
throw GrpcUtil.statusRuntimeException(Code.INVALID_ARGUMENT, "Autocomplete command missing request"); | ||
} | ||
} | ||
} | ||
|
@@ -101,33 +127,18 @@ private void getCompletionItems(GetCompletionItemsRequest request, | |
StreamObserver<AutoCompleteResponse> responseObserver) { | ||
final ScriptSession scriptSession = exportedConsole.get(); | ||
try (final SafeCloseable ignored = scriptSession.getExecutionContext().open()) { | ||
|
||
PyObject completer = (PyObject) scriptSession.getVariable("jedi_settings"); | ||
boolean canJedi = completer.callMethod("is_enabled").getBooleanValue(); | ||
if (!canJedi) { | ||
log.trace().append("Ignoring completion request because jedi is disabled").endl(); | ||
// send back an empty, failed response... | ||
safelyExecuteLocked(responseObserver, | ||
() -> responseObserver.onNext(AutoCompleteResponse.newBuilder() | ||
.setCompletionItems(GetCompletionItemsResponse.newBuilder() | ||
.setSuccess(false) | ||
.setRequestId(request.getRequestId())) | ||
.build())); | ||
return; | ||
} | ||
Comment on lines
-105
to
-117
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what happens if client uses |
||
final VersionedTextDocumentIdentifier doc = request.getTextDocument(); | ||
final Position pos = request.getPosition(); | ||
final long startNano = System.nanoTime(); | ||
|
||
if (log.isTraceEnabled()) { | ||
String text = completer.call("get_doc", doc.getUri()).getStringValue(); | ||
String text = jedi.get_doc(doc.getUri()); | ||
log.trace().append("Completion version ").append(doc.getVersion()) | ||
.append(" has source code:").append(text).endl(); | ||
} | ||
final PyObject results = completer.callMethod("do_completion", doc.getUri(), doc.getVersion(), | ||
// our java is 0-indexed lines, 1-indexed chars. jedi is 1-indexed-both. | ||
// we'll keep that translation ugliness to the in-java result-processing. | ||
pos.getLine() + 1, pos.getCharacter()); | ||
// our java is 0-indexed lines, 1-indexed chars. jedi is 1-indexed-both. | ||
// we'll keep that translation ugliness to the in-java result-processing. | ||
final PyObject results = | ||
jedi.do_completion(doc.getUri(), doc.getVersion(), pos.getLine() + 1, pos.getCharacter()); | ||
if (!results.isList()) { | ||
throw new UnsupportedOperationException( | ||
"Expected list from jedi_settings.do_completion, got " + results.call("repr")); | ||
|
@@ -214,7 +225,7 @@ private void getCompletionItems(GetCompletionItemsRequest request, | |
} | ||
} | ||
|
||
private String toMillis(final long totalNanos) { | ||
private static String toMillis(final long totalNanos) { | ||
StringBuilder totalNano = new StringBuilder(Long.toString(totalNanos)); | ||
while (totalNano.length() < 7) { | ||
totalNano.insert(0, "0"); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just curious... why move the instance location? the object is designed such that if customer wants to turn off autocomplete, they need to access this instance to set the mode to off. Just want to make sure that is clear when you are making decisions about where things should live.