-
Notifications
You must be signed in to change notification settings - Fork 137
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
On demand indexing #1260
On demand indexing #1260
Changes from 1 commit
2ff5acf
a9882c5
7dd5582
fffe202
75d30f2
6c20c94
92c7e62
055a922
8934fad
0af544f
9a478cc
03579f9
9c73c66
1a59fd8
afecb31
056aa61
5ba84b8
ff720ce
de7f47a
99ac925
b9c83d2
fc9c456
cec3040
d136370
a003d31
dc94b07
41ca00b
413b5dd
5416d7a
d98eb18
d59306d
8d34dfa
3971114
bbcb689
f054f92
8a91a23
161dc2c
c505eff
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 |
---|---|---|
@@ -0,0 +1,114 @@ | ||
%%%============================================================================= | ||
%%% @doc Buffer edits to an open buffer to avoid re-indexing too often. | ||
%%% @end | ||
%%%============================================================================= | ||
-module(els_buffer_server). | ||
|
||
%%============================================================================== | ||
%% API | ||
%%============================================================================== | ||
-export([ new/2 | ||
, stop/1 | ||
, apply_edits/2 | ||
, flush/1 | ||
]). | ||
|
||
-export([ start_link/2 ]). | ||
|
||
%%============================================================================== | ||
%% Callbacks for the gen_server behaviour | ||
%%============================================================================== | ||
-behaviour(gen_server). | ||
-export([ init/1 | ||
, handle_call/3 | ||
, handle_cast/2 | ||
, handle_info/2 | ||
]). | ||
|
||
%%============================================================================== | ||
%% Macro Definitions | ||
%%============================================================================== | ||
-define(FLUSH_DELAY, 200). %% ms | ||
|
||
%%============================================================================== | ||
%% Type Definitions | ||
%%============================================================================== | ||
-type text() :: binary(). | ||
-type state() :: #{ uri := uri() | ||
, text := text() | ||
, ref := undefined | reference() | ||
}. | ||
-type buffer() :: pid(). | ||
|
||
%%============================================================================== | ||
%% Includes | ||
%%============================================================================== | ||
-include_lib("kernel/include/logger.hrl"). | ||
-include("els_lsp.hrl"). | ||
|
||
%%============================================================================== | ||
%% API | ||
%%============================================================================== | ||
-spec new(uri(), text()) -> {ok, pid()}. | ||
new(Uri, Text) -> | ||
supervisor:start_child(els_buffer_sup, [Uri, Text]). | ||
|
||
-spec stop(buffer()) -> ok. | ||
stop(Buffer) -> | ||
supervisor:terminate_child(els_buffer_sup, Buffer). | ||
|
||
-spec apply_edits(buffer(), [els_text:edit()]) -> ok. | ||
apply_edits(Buffer, Edits) -> | ||
gen_server:cast(Buffer, {apply_edits, Edits}). | ||
|
||
-spec flush(buffer()) -> ok. | ||
flush(Buffer) -> | ||
gen_server:call(Buffer, {flush}). | ||
|
||
-spec start_link(uri(), text()) -> {ok, buffer()}. | ||
start_link(Uri, Text) -> | ||
gen_server:start_link(?MODULE, {Uri, Text}, []). | ||
|
||
%%============================================================================== | ||
%% Callbacks for the gen_server behaviour | ||
%%============================================================================== | ||
-spec init({uri(), text()}) -> {ok, state()}. | ||
init({Uri, Text}) -> | ||
do_flush(Uri, Text), | ||
{ok, #{ uri => Uri, text => Text, ref => undefined }}. | ||
|
||
-spec handle_call(any(), {pid(), any()}, state()) -> {reply, any(), state()}. | ||
handle_call({flush}, _From, #{uri := Uri, text := Text} = State) -> | ||
?LOG_DEBUG("[~p] Flushing request [uri=~p]", [?MODULE, Uri]), | ||
do_flush(Uri, Text), | ||
{reply, ok, State}; | ||
handle_call(Request, _From, State) -> | ||
{reply, {not_implemented, Request}, State}. | ||
|
||
-spec handle_cast(any(), state()) -> {noreply, state()}. | ||
handle_cast({apply_edits, Edits}, #{uri := Uri} = State) -> | ||
?LOG_DEBUG("[~p] Applying edits [uri=~p]", [?MODULE, Uri]), | ||
#{text := Text0, ref := Ref0} = State, | ||
case Ref0 of | ||
undefined -> ok; | ||
_ -> erlang:cancel_timer(Ref0) | ||
end, | ||
Text = els_text:apply_edits(Text0, Edits), | ||
Ref = erlang:send_after(?FLUSH_DELAY, self(), flush), | ||
{noreply, State#{text => Text, ref => Ref}}. | ||
|
||
-spec handle_info(any(), state()) -> {noreply, state()}. | ||
handle_info(flush, #{uri := Uri, text := Text} = State) -> | ||
?LOG_DEBUG("[~p] Scheduled flushing [uri=~p]", [?MODULE, Uri]), | ||
do_flush(Uri, Text), | ||
{noreply, State}; | ||
handle_info(_Request, State) -> | ||
{noreply, State}. | ||
|
||
%%============================================================================== | ||
%% Internal Functions | ||
%%============================================================================== | ||
-spec do_flush(uri(), text()) -> ok. | ||
do_flush(Uri, Text) -> | ||
{ok, Document} = els_utils:lookup_document(Uri), | ||
els_indexing:deep_index(Document#{text => Text, buffer => self()}). |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
%%============================================================================== | ||
%% Supervisor for Buffers | ||
%%============================================================================== | ||
-module(els_buffer_sup). | ||
|
||
%%============================================================================== | ||
%% Behaviours | ||
%%============================================================================== | ||
-behaviour(supervisor). | ||
|
||
%%============================================================================== | ||
%% Exports | ||
%%============================================================================== | ||
|
||
%% API | ||
-export([ start_link/0 ]). | ||
|
||
%% Supervisor Callbacks | ||
-export([ init/1 ]). | ||
|
||
%%============================================================================== | ||
%% Defines | ||
%%============================================================================== | ||
-define(SERVER, ?MODULE). | ||
|
||
%%============================================================================== | ||
%% API | ||
%%============================================================================== | ||
-spec start_link() -> {ok, pid()}. | ||
start_link() -> | ||
supervisor:start_link({local, ?SERVER}, ?MODULE, []). | ||
|
||
%%============================================================================== | ||
%% Supervisor callbacks | ||
%%============================================================================== | ||
-spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. | ||
init([]) -> | ||
SupFlags = #{ strategy => simple_one_for_one | ||
, intensity => 5 | ||
, period => 60 | ||
}, | ||
ChildSpecs = [#{ id => els_buffer_sup | ||
, start => {els_buffer_server, start_link, []} | ||
, restart => temporary | ||
, shutdown => 5000 | ||
}], | ||
{ok, {SupFlags, ChildSpecs}}. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,8 +44,17 @@ handle_request({completion, Params}, State) -> | |
} | ||
, <<"textDocument">> := #{<<"uri">> := Uri} | ||
} = Params, | ||
ok = els_index_buffer:flush(Uri), | ||
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 bit of context may help here. Completion requests typically come at the same time as edits. With the current implementation of the The completion provider assumes a specific length in the line when reading the text after the flush, but it's not guaranteed that all edits are applied. Fast typers can see the provider (and eventually the whole server) crashing. With the new approach, the idea is to block callers of the flush for a certain amount of time (right now 200ms), but if edits keep coming the count restarts. This should also prevent indexing happening multiple times. 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. Hmm... I'm not sure I fully understand how the processing of everything happens. Can you confirm that |
||
{ok, #{text := Text} = Document} = els_utils:lookup_document(Uri), | ||
{ok, Document} = els_utils:lookup_document(Uri), | ||
#{text := Text, buffer := Buffer} = Document, | ||
%% Ensure there are no pending changes. This causes a heavy sync | ||
%% point, since for a big module indexing could take a while. Maybe | ||
%% just do a 'soft' flush and avoid indexing here? | ||
case Buffer of | ||
undefined -> | ||
ok; | ||
_ -> | ||
ok = els_buffer_server:flush(Buffer) | ||
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. I wonder if we cannot be even more clever here and to make flushing delayed. Looking at the LSP, I see that we can return partial completion results. So what we can do to increase responsiveness is 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. Partial results are not yet implemented, I will add this as a follow up. |
||
end, | ||
Context = maps:get( <<"context">> | ||
, Params | ||
, #{ <<"triggerKind">> => ?COMPLETION_TRIGGER_KIND_INVOKED } | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,6 +45,7 @@ | |
-type id() :: atom(). | ||
-type kind() :: module | header | other. | ||
-type source() :: otp | app | dep. | ||
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. We were not storing references for OTP modules during the initial indexing. |
||
-type buffer() :: pid(). | ||
-export_type([source/0]). | ||
|
||
%%============================================================================== | ||
|
@@ -58,6 +59,7 @@ | |
, md5 :: binary() | '_' | ||
, pois :: [poi()] | '_' | ondemand | ||
, source :: source() | '$2' | ||
, buffer :: buffer() | '_' | undefined | ||
}). | ||
-type els_dt_document() :: #els_dt_document{}. | ||
|
||
|
@@ -68,6 +70,7 @@ | |
, md5 => binary() | ||
, pois => [poi()] | ondemand | ||
, source => source() | ||
, buffer => buffer() | undefined | ||
}. | ||
-export_type([ id/0 | ||
, item/0 | ||
|
@@ -97,6 +100,7 @@ from_item(#{ uri := Uri | |
, md5 := MD5 | ||
, pois := POIs | ||
, source := Source | ||
, buffer := Buffer | ||
}) -> | ||
#els_dt_document{ uri = Uri | ||
, id = Id | ||
|
@@ -105,6 +109,7 @@ from_item(#{ uri := Uri | |
, md5 = MD5 | ||
, pois = POIs | ||
, source = Source | ||
, buffer = Buffer | ||
}. | ||
|
||
-spec to_item(els_dt_document()) -> item(). | ||
|
@@ -115,6 +120,7 @@ to_item(#els_dt_document{ uri = Uri | |
, md5 = MD5 | ||
, pois = POIs | ||
, source = Source | ||
, buffer = Buffer | ||
}) -> | ||
#{ uri => Uri | ||
, id => Id | ||
|
@@ -123,6 +129,7 @@ to_item(#els_dt_document{ uri = Uri | |
, md5 => MD5 | ||
, pois => POIs | ||
, source => Source | ||
, buffer => Buffer | ||
}. | ||
|
||
-spec insert(item()) -> ok | {error, any()}. | ||
|
@@ -162,6 +169,7 @@ new(Uri, Text, Id, Kind, Source) -> | |
, md5 => MD5 | ||
, pois => ondemand | ||
, source => Source | ||
, buffer => undefined | ||
}. | ||
|
||
%% @doc Returns the list of POIs for the current document | ||
|
@@ -221,10 +229,12 @@ find_candidates(Pattern) -> | |
%% when Source =/= otp -> {Uri, Text} end). | ||
MS = [{#els_dt_document{ uri = '$1' | ||
, source = '$2' | ||
, buffer = '_' | ||
, kind = '_' | ||
, text = '$3' | ||
, md5 = '_' | ||
,pois = '_'} | ||
, pois = '_' | ||
} | ||
, [{'=/=', '$2', otp}] | ||
, [{{'$1', '$3'}}]}], | ||
All = ets:select(name(), MS), | ||
|
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.
Could you explain a bit why we need a buffer server in the first place, rather than keeping the text directly and using plain functions for processing edits?
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.
The buffer was introduced as part of support for incremental text sync and I just ported it to a
gen_server
, so it's not new with this feature.The main intention was to buffer
didChange
events, so that re-indexing would be retriggered not too often (a didChange event is sent at every keystroke). But I agree that it complicates things and probably introduces some races.