-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
RPC method performance improvements (#46)
* feat: experimental RPC request cancellation * chore: ignore nix stuff * chore: trace * feat: memoize * chore: adaptations for nightly-2024-02-08 * chore: bump to 2024-02-24 * fix: demo * chore: bump to v4.7.0-rc1 --------- Co-authored-by: Scott Morrison <[email protected]>
- Loading branch information
Showing
13 changed files
with
193 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,3 +3,4 @@ | |
/widget/dist | ||
/widget/node_modules | ||
/widget/*.hash | ||
.direnv/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import Lean.Data.Json.FromToJson | ||
import Lean.Server.Rpc.RequestHandling | ||
import ProofWidgets.Compat | ||
|
||
/-! Experimental support for cancellable RPC requests. | ||
Note: Cancellation should eventually become a feature of the core RPC protocol, | ||
and the requests map should be stored in `RequestM`, | ||
or somewhere in the server anyway. -/ | ||
|
||
namespace ProofWidgets | ||
open Lean Server Meta | ||
|
||
abbrev RequestId := Nat | ||
structure CancellableTask where | ||
task : Task (Except RequestError (LazyEncodable Json)) | ||
/- Note: we cannot just `IO.cancel task` because it is a result of `map`. | ||
See https://leanprover.zulipchat.com/#narrow/stream/270676-lean4/topic/Should.20cancelling.20a.20purely.20mapped.20task.20cancel.20the.20original.3F -/ | ||
cancel : IO Unit | ||
|
||
/-- Maps the ID of each currently executing request to its task. -/ | ||
initialize runningRequests : | ||
IO.Ref (RequestId × HashMap RequestId CancellableTask) ← | ||
IO.mkRef (0, HashMap.empty) | ||
|
||
/-- Transforms a request handler returning `β` | ||
into one that returns immediately with a `RequestId`. | ||
The ID uniquely identifies the running request: | ||
its results can be retrieved using `checkRequest`, | ||
and it can be cancelled using `cancelRequest`. -/ | ||
def mkCancellable [RpcEncodable β] (handler : α → RequestM (RequestTask β)) : | ||
α → RequestM (RequestTask RequestId) := fun a => do | ||
RequestM.asTask do | ||
let t ← handler a | ||
let t' := t.map (·.map rpcEncode) | ||
runningRequests.modifyGet fun (id, m) => | ||
(id, (id+1, m.insert id ⟨t', IO.cancel t⟩)) | ||
|
||
/-- Cancel the request with ID `rid`. | ||
Does nothing if `rid` is invalid. -/ | ||
@[server_rpc_method] | ||
def cancelRequest (rid : RequestId) : RequestM (RequestTask String) := do | ||
RequestM.asTask do | ||
let t? ← runningRequests.modifyGet fun (id, m) => (m.find? rid, (id, m.erase rid)) | ||
if let some t := t? then | ||
t.cancel | ||
return "ok" | ||
|
||
/-- The status of a running cancellable request. -/ | ||
inductive CheckRequestResponse | ||
| running | ||
| done (result : LazyEncodable Json) | ||
deriving RpcEncodable | ||
|
||
/-- Check whether a request has finished computing, | ||
and return the response if so. | ||
The request is removed from `runningRequests` the first time it is checked | ||
and found to have finished. | ||
Throws an error if the `rid` is invalid, | ||
or if the request itself threw an error. -/ | ||
/- NOTE: a notification-based version would be better than this polling-based one. | ||
But we cannot include RPC references in notifications atm; | ||
another possible addition to the RPC protocol? -/ | ||
@[server_rpc_method] | ||
def checkRequest (rid : RequestId) : RequestM (RequestTask CheckRequestResponse) := do | ||
RequestM.asTask do | ||
let (_, m) ← runningRequests.get | ||
match m.find? rid with | ||
| none => | ||
throw $ RequestError.invalidParams | ||
s!"Request '{rid}' has already finished, or the ID is invalid." | ||
| some t => | ||
if !(← IO.hasFinished t.task) then | ||
return .running | ||
runningRequests.modify fun (id, m) => (id, m.erase rid) | ||
match t.task.get with | ||
| .error e => throw e | ||
| .ok v => return .done v | ||
|
||
def cancellableSuffix : Name := `_cancellable | ||
|
||
initialize | ||
registerBuiltinAttribute { | ||
name := `server_rpc_method_cancellable | ||
descr := "Like `server_rpc_method`, \ | ||
but requests for this method can be cancelled. \ | ||
The method should check for that using `IO.checkCanceled`. \ | ||
Cancellable methods are invoked differently from JavaScript: \ | ||
see `callCancellable` in `cancellable.ts`." | ||
applicationTime := AttributeApplicationTime.afterCompilation | ||
add := fun decl _ _ => Prod.fst <$> MetaM.run do | ||
let name := decl ++ cancellableSuffix | ||
let value ← mkAppM ``mkCancellable #[mkConst decl] | ||
addAndCompile $ .defnDecl { | ||
name | ||
levelParams := [] | ||
type := ← inferType value | ||
value | ||
hints := .opaque | ||
safety := .safe | ||
} | ||
registerRpcProcedure name | ||
} | ||
|
||
end ProofWidgets |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,4 @@ | ||
import Lean.Server.Rpc.Basic | ||
import Std.Data.Json | ||
|
||
import ProofWidgets.Data.Html | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
leanprover/lean4:v4.6.0 | ||
leanprover/lean4:v4.7.0-rc1 |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { RpcSessionAtPos } from '@leanprover/infoview' | ||
|
||
type RequestId = string | ||
type CheckRequestResponse<S> = | ||
'running' | { done: { result: S } } | ||
|
||
/** An object for passing a function by reference. */ | ||
export type Fn = { | ||
fn: () => void | ||
} | ||
|
||
/** Returns a promise of a response to this request | ||
* as well as a function you can call to cancel the request. | ||
* It is request-specific what a cancelled request resolves with: | ||
* it could ignore the cancellation altogether, | ||
* return a null result, | ||
* or throw an error. */ | ||
export function callCancellable<T, S>(rs: RpcSessionAtPos, name: string, params: T): | ||
[Promise<S>, Fn] { | ||
const cancelFn = { | ||
fn: () => {} | ||
} | ||
const promise = new Promise<S>(async (resolve, reject) => { | ||
const id: RequestId = await rs.call(name, params) | ||
const i = window.setInterval(async () => { | ||
try { | ||
const response : CheckRequestResponse<S> = await rs.call('ProofWidgets.checkRequest', id) | ||
if (response === 'running') return | ||
window.clearInterval(i) | ||
resolve(response.done.result) | ||
} catch (e) { | ||
window.clearInterval(i) | ||
reject(e) | ||
} | ||
}, 100) | ||
cancelFn.fn = () => { | ||
void rs.call('ProofWidgets.cancelRequest', id) | ||
} | ||
}) | ||
return [promise, cancelFn] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,39 @@ | ||
import * as React from 'react' | ||
import { DocumentPosition, RpcContext, mapRpcError, useAsyncPersistent } from '@leanprover/infoview' | ||
import { Html, default as HtmlDisplay } from './htmlDisplay' | ||
import { Fn, callCancellable } from './cancellable' | ||
|
||
declare type Props = any & { pos : DocumentPosition }; | ||
// The string template is replaced in OfRpcMethod.lean | ||
// The string template is replaced in OfRpcMethod.lean. | ||
const RPC_METHOD = '$RPC_METHOD' | ||
// HACK: TS runs dead-code elimination | ||
// and eliminates the `callCancellable` branch if we use a string literal, | ||
// so use a runtime value. | ||
const RPC_CANCELLABLE = window.toString() | ||
|
||
export default function(props: Props) { | ||
export default React.memo((props: Props) => { | ||
const rs = React.useContext(RpcContext) | ||
const st = useAsyncPersistent<Html>(async () => { | ||
return await rs.call(RPC_METHOD, props) | ||
const cancelRef = React.useRef<Fn>({ fn: () => {} }) | ||
const st = useAsyncPersistent<Html>(async () => { | ||
if (RPC_CANCELLABLE === 'true') { | ||
cancelRef.current.fn() | ||
const [res, cancel] = callCancellable<Props, Html>(rs, RPC_METHOD, props) | ||
cancelRef.current = cancel | ||
return res | ||
} else | ||
return rs.call<Props, Html>(RPC_METHOD, props) | ||
}, [rs, props]) | ||
|
||
// This effect runs once on startup, does nothing, | ||
// and sets up a cleanup function that runs when the component is unmounted. | ||
React.useEffect(() => { | ||
return () => { cancelRef.current.fn() } | ||
}, []) | ||
|
||
return st.state === 'rejected' ? | ||
<p style={{color: 'red'}}>{mapRpcError(st.error).message}</p> | ||
: st.state === 'loading' ? | ||
<>Loading..</> | ||
: | ||
<HtmlDisplay pos={props.pos} html={st.value} /> | ||
} | ||
}) |