Skip to content

Commit a9a806c

Browse files
authored
Performance oriented fixes. (#841)
* Perf: Precalculate contexts We were calculating the context of the cursor once per suggestion, this would take a couple dozen milliseconds, but for a module that had a lot of functions, this would add up. Precalculating them saved a lot of time. * Use new Mix.Tasks.Format Jose added a function that allows us to get a formatter without having to lock on the build. This reduces the formatting time significantly, and we no longer have that frustrating freeze on save. --------- Co-authored-by: scohen <scohen@users.noreply.github.com>
1 parent bc36cc4 commit a9a806c

File tree

11 files changed

+1256
-88
lines changed

11 files changed

+1256
-88
lines changed

apps/common/lib/future/mix/tasks/format.ex

Lines changed: 1047 additions & 0 deletions
Large diffs are not rendered by default.

apps/common/lib/lexical/ast/env.ex

Lines changed: 45 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ defmodule Lexical.Ast.Env do
2121
:suffix,
2222
:position,
2323
:position_module,
24-
:zero_based_character
24+
:zero_based_character,
25+
:detected_contexts
2526
]
2627

2728
@type t :: %__MODULE__{
@@ -33,7 +34,8 @@ defmodule Lexical.Ast.Env do
3334
suffix: String.t(),
3435
position: Position.t(),
3536
position_module: String.t(),
36-
zero_based_character: non_neg_integer()
37+
zero_based_character: non_neg_integer(),
38+
detected_contexts: %{atom() => boolean()}
3739
}
3840

3941
@type token_value :: String.t() | charlist() | atom()
@@ -87,6 +89,8 @@ defmodule Lexical.Ast.Env do
8789
zero_based_character: zero_based_character
8890
}
8991

92+
env = detect_contexts(env)
93+
9094
{:ok, env}
9195

9296
_ ->
@@ -107,81 +111,55 @@ defmodule Lexical.Ast.Env do
107111
end
108112
end
109113

114+
@detectors %{
115+
:alias => Detection.Alias,
116+
:behaviour => {Detection.ModuleAttribute, [:behaviour]},
117+
:bitstring => Detection.Bitstring,
118+
:callback => {Detection.ModuleAttribute, [:callback]},
119+
:comment => Detection.Comment,
120+
:doc => {Detection.ModuleAttribute, [:doc]},
121+
:function_capture => Detection.FunctionCapture,
122+
:impl => {Detection.ModuleAttribute, [:impl]},
123+
:import => Detection.Import,
124+
:macrocallback => {Detection.ModuleAttribute, [:macrocallback]},
125+
:moduledoc => {Detection.ModuleAttribute, [:moduledoc]},
126+
:pipe => Detection.Pipe,
127+
:require => Detection.Require,
128+
:spec => Detection.Spec,
129+
:string => Detection.String,
130+
:struct_field_key => Detection.StructFieldKey,
131+
:struct_field_value => Detection.StructFieldValue,
132+
:struct_fields => Detection.StructFields,
133+
:struct_reference => Detection.StructReference,
134+
:type => Detection.Type,
135+
:use => Detection.Use
136+
}
137+
138+
def detect_contexts(%__MODULE__{} = env) do
139+
detected_contexts =
140+
Map.new(@detectors, fn
141+
{context_name, {detector, extra_args}} ->
142+
{context_name, apply(detector, :detected?, [env.analysis, env.position | extra_args])}
143+
144+
{context_name, detector} ->
145+
{context_name, detector.detected?(env.analysis, env.position)}
146+
end)
147+
148+
%__MODULE__{env | detected_contexts: detected_contexts}
149+
end
150+
110151
@spec in_context?(t, context_type) :: boolean()
111152
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
112153
def in_context?(%__MODULE__{} = env, context_type) do
113154
analysis = env.analysis
114155
position = env.position
115156

116157
case context_type do
117-
:alias ->
118-
Detection.Alias.detected?(analysis, position)
119-
120-
:behaviour ->
121-
Detection.ModuleAttribute.detected?(analysis, position, :behaviour)
122-
123-
:bitstring ->
124-
Detection.Bitstring.detected?(analysis, position)
125-
126-
:callback ->
127-
Detection.ModuleAttribute.detected?(analysis, position, :callback)
128-
129-
:comment ->
130-
Detection.Comment.detected?(analysis, position)
131-
132-
:doc ->
133-
Detection.ModuleAttribute.detected?(analysis, position, :doc)
134-
135-
:function_capture ->
136-
Detection.FunctionCapture.detected?(analysis, position)
137-
138-
:impl ->
139-
Detection.ModuleAttribute.detected?(analysis, position, :impl)
140-
141-
:import ->
142-
Detection.Import.detected?(analysis, position)
143-
144-
:module_attribute ->
145-
Detection.ModuleAttribute.detected?(analysis, position)
146-
147158
{:module_attribute, name} ->
148159
Detection.ModuleAttribute.detected?(analysis, position, name)
149160

150-
:macrocallback ->
151-
Detection.ModuleAttribute.detected?(analysis, position, :macrocallback)
152-
153-
:moduledoc ->
154-
Detection.ModuleAttribute.detected?(analysis, position, :moduledoc)
155-
156-
:pipe ->
157-
Detection.Pipe.detected?(analysis, position)
158-
159-
:require ->
160-
Detection.Require.detected?(analysis, position)
161-
162-
:spec ->
163-
Detection.Spec.detected?(analysis, position)
164-
165-
:string ->
166-
Detection.String.detected?(analysis, position)
167-
168-
:struct_fields ->
169-
Detection.StructFields.detected?(analysis, position)
170-
171-
:struct_field_key ->
172-
Detection.StructFieldKey.detected?(analysis, position)
173-
174-
:struct_field_value ->
175-
Detection.StructFieldValue.detected?(analysis, position)
176-
177-
:struct_reference ->
178-
Detection.StructReference.detected?(analysis, position)
179-
180-
:type ->
181-
Detection.Type.detected?(analysis, position)
182-
183-
:use ->
184-
Detection.Use.detected?(analysis, position)
161+
context_type ->
162+
Map.get(env.detected_contexts, context_type)
185163
end
186164
end
187165

apps/remote_control/lib/lexical/remote_control.ex

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,23 @@ defmodule Lexical.RemoteControl do
7575
end
7676
end
7777

78+
def deps_paths do
79+
case :persistent_term.get({__MODULE__, :deps_paths}, :error) do
80+
:error ->
81+
{:ok, deps_paths} =
82+
RemoteControl.Mix.in_project(fn _ ->
83+
Mix.Task.run("loadpaths")
84+
Mix.Project.deps_paths()
85+
end)
86+
87+
:persistent_term.put({__MODULE__, :deps_paths}, deps_paths)
88+
deps_paths
89+
90+
deps_paths ->
91+
deps_paths
92+
end
93+
end
94+
7895
def with_lock(lock_type, func) do
7996
:global.trans({lock_type, self()}, func, [Node.self()])
8097
end

apps/remote_control/lib/lexical/remote_control/build/project.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,10 @@ defmodule Lexical.RemoteControl.Build.Project do
9191
Mix.Task.run(:loadconfig)
9292
end
9393

94-
with_progress "mix deps.compile", fn ->
95-
Mix.Task.run("deps.safe_compile", ~w(--skip-umbrella-children))
94+
unless Elixir.Features.compile_keeps_current_directory?() do
95+
with_progress "mix deps.compile", fn ->
96+
Mix.Task.run("deps.safe_compile", ~w(--skip-umbrella-children))
97+
end
9698
end
9799

98100
with_progress "loading plugins", fn ->

apps/remote_control/lib/lexical/remote_control/code_mod/format.ex

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,11 @@ defmodule Lexical.RemoteControl.CodeMod.Format do
164164

165165
{formatter_function, opts} =
166166
if RemoteControl.project_node?() do
167-
case RemoteControl.Mix.in_project(project, fetch_formatter) do
167+
case mix_formatter_from_task(project, file_path) do
168168
{:ok, result} ->
169169
result
170170

171-
_error ->
171+
:error ->
172172
formatter_opts =
173173
case find_formatter_exs(project, file_path) do
174174
{:ok, opts} ->
@@ -211,13 +211,19 @@ defmodule Lexical.RemoteControl.CodeMod.Format do
211211
end
212212

213213
defp do_find_formatter_exs(root_path, current_path) do
214-
with :error <- formatter_exs_contents(current_path) do
215-
parent =
216-
current_path
217-
|> Path.join("..")
218-
|> Path.expand()
214+
if File.exists?(current_path) do
215+
with :error <- formatter_exs_contents(current_path) do
216+
parent =
217+
current_path
218+
|> Path.join("..")
219+
|> Path.expand()
219220

220-
do_find_formatter_exs(root_path, parent)
221+
do_find_formatter_exs(root_path, parent)
222+
end
223+
else
224+
# the current path doesn't exist, it doesn't make sense to keep looking
225+
# for the .formatter.exs in its parents. Look for one in the root directory
226+
do_find_formatter_exs(root_path, Path.join(root_path, ".formatter.exs"))
221227
end
222228
end
223229

@@ -235,4 +241,23 @@ defmodule Lexical.RemoteControl.CodeMod.Format do
235241
:error
236242
end
237243
end
244+
245+
defp mix_formatter_from_task(%Project{} = project, file_path) do
246+
try do
247+
root_path = Project.root_path(project)
248+
deps_paths = RemoteControl.deps_paths()
249+
250+
formatter_and_opts =
251+
Mix.Tasks.Future.Format.formatter_for_file(file_path,
252+
root: root_path,
253+
deps_paths: deps_paths,
254+
plugin_loader: fn plugins -> Enum.filter(plugins, &Code.ensure_loaded?/1) end
255+
)
256+
257+
{:ok, formatter_and_opts}
258+
rescue
259+
_ ->
260+
:error
261+
end
262+
end
238263
end

apps/remote_control/lib/lexical/remote_control/completion.ex

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ defmodule Lexical.RemoteControl.Completion do
88
alias Lexical.RemoteControl.Completion.Candidate
99

1010
import Document.Line
11+
import Lexical.Logging
1112

1213
def elixir_sense_expand(%Env{} = env) do
1314
{doc_string, position} = strip_struct_operator(env)
@@ -19,11 +20,21 @@ defmodule Lexical.RemoteControl.Completion do
1920
if String.trim(hint) == "" do
2021
[]
2122
else
22-
{_formatter, opts} = Format.formatter_for_file(env.project, env.document.path)
23+
{_formatter, opts} =
24+
timed_log("formatter for file", fn ->
25+
Format.formatter_for_file(env.project, env.document.path)
26+
end)
27+
2328
locals_without_parens = Keyword.fetch!(opts, :locals_without_parens)
2429

25-
for suggestion <- ElixirSense.suggestions(doc_string, line, character),
26-
candidate = from_elixir_sense(suggestion, locals_without_parens),
30+
for suggestion <-
31+
timed_log("ES suggestions", fn ->
32+
ElixirSense.suggestions(doc_string, line, character)
33+
end),
34+
candidate =
35+
timed_log("from_elixir_sense", fn ->
36+
from_elixir_sense(suggestion, locals_without_parens)
37+
end),
2738
candidate != nil do
2839
candidate
2940
end

apps/server/lib/lexical/server/code_intelligence/completion.ex

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ defmodule Lexical.Server.CodeIntelligence.Completion do
3636
case Env.new(project, analysis, position) do
3737
{:ok, env} ->
3838
completions = completions(project, env, context)
39-
Logger.info("Emitting completions: #{inspect(completions)}")
39+
log_candidates(completions)
4040
maybe_to_completion_list(completions)
4141

4242
{:error, _} = error ->
@@ -45,6 +45,19 @@ defmodule Lexical.Server.CodeIntelligence.Completion do
4545
end
4646
end
4747

48+
defp log_candidates(candidates) do
49+
log_iolist =
50+
Enum.reduce(candidates, ["Emitting Completions: ["], fn %Completion.Item{} = completion,
51+
acc ->
52+
name = Map.get(completion, :name) || Map.get(completion, :label)
53+
kind = completion |> Map.get(:kind, :unknown) |> to_string()
54+
55+
[acc, [kind, ":", name], " "]
56+
end)
57+
58+
Logger.info([log_iolist, "]"])
59+
end
60+
4861
defp completions(%Project{} = project, %Env{} = env, %Completion.Context{} = context) do
4962
prefix_tokens = Env.prefix_tokens(env, 1)
5063

@@ -152,7 +165,7 @@ defmodule Lexical.Server.CodeIntelligence.Completion do
152165
%Env{} = env,
153166
%Completion.Context{} = context
154167
) do
155-
Logger.info("Local completions are #{inspect(local_completions)}")
168+
debug_local_completions(local_completions)
156169

157170
for result <- local_completions,
158171
displayable?(project, result),
@@ -163,6 +176,30 @@ defmodule Lexical.Server.CodeIntelligence.Completion do
163176
end
164177
end
165178

179+
defp debug_local_completions(completions) do
180+
completions_by_type =
181+
Enum.group_by(completions, fn %candidate_module{} ->
182+
candidate_module
183+
|> Atom.to_string()
184+
|> String.split(".")
185+
|> List.last()
186+
|> String.downcase()
187+
end)
188+
189+
log_iodata =
190+
Enum.reduce(completions_by_type, ["Local completions are: ["], fn {type, completions},
191+
acc ->
192+
names =
193+
Enum.map_join(completions, ", ", fn candidate ->
194+
Map.get(candidate, :name) || Map.get(candidate, :detail)
195+
end)
196+
197+
[acc, [type, ": (", names], ") "]
198+
end)
199+
200+
Logger.info([log_iodata, "]"])
201+
end
202+
166203
defp to_completion_item(candidate, env) do
167204
candidate
168205
|> Translatable.translate(Builder, env)

0 commit comments

Comments
 (0)