Skip to content

Commit ca9e150

Browse files
authored
Experiment in lock-free module invalidation (#9639)
1. Experimenting with invalidating modules' indexes without requiring full write-context locks. That should significantly improve the execution. 2. Improving performance by making background job executor run in a larger threadpool than 1.
1 parent 0ee113a commit ca9e150

File tree

15 files changed

+270
-105
lines changed

15 files changed

+270
-105
lines changed

engine/runtime-instrument-common/src/main/java/org/enso/interpreter/instrument/command/InvalidateModulesIndexCommand.java

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,16 @@ public Future<BoxedUnit> executeAsynchronously(RuntimeContext ctx, ExecutionCont
3030
return Future.apply(
3131
() -> {
3232
TruffleLogger logger = ctx.executionService().getLogger();
33-
long writeCompilationLockTimestamp = ctx.locking().acquireWriteCompilationLock();
3433
try {
34+
logger.log(Level.FINE, "Invalidating modules, cancelling background jobs");
3535
ctx.jobControlPlane().stopBackgroundJobs();
3636
ctx.jobControlPlane().abortBackgroundJobs(DeserializeLibrarySuggestionsJob.class);
3737

3838
EnsoContext context = ctx.executionService().getContext();
39-
context.getTopScope().getModules().forEach(module -> module.setIndexed(false));
39+
context
40+
.getTopScope()
41+
.getModules()
42+
.forEach(module -> ctx.state().suggestions().markIndexAsDirty(module));
4043

4144
context
4245
.getPackageRepository()
@@ -47,19 +50,9 @@ public Future<BoxedUnit> executeAsynchronously(RuntimeContext ctx, ExecutionCont
4750
.runBackground(new DeserializeLibrarySuggestionsJob(pkg.libraryName()));
4851
return BoxedUnit.UNIT;
4952
});
50-
51-
reply(new Runtime$Api$InvalidateModulesIndexResponse(), ctx);
5253
} finally {
53-
ctx.locking().releaseWriteCompilationLock();
54-
logger.log(
55-
Level.FINEST,
56-
"Kept write compilation lock [{0}] for {1} milliseconds.",
57-
new Object[] {
58-
this.getClass().getSimpleName(),
59-
System.currentTimeMillis() - writeCompilationLockTimestamp
60-
});
54+
reply(new Runtime$Api$InvalidateModulesIndexResponse(), ctx);
6155
}
62-
6356
return BoxedUnit.UNIT;
6457
},
6558
ec);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package org.enso.interpreter.instrument.execution;
2+
3+
import java.util.concurrent.ConcurrentHashMap;
4+
import java.util.concurrent.ConcurrentMap;
5+
import java.util.concurrent.atomic.AtomicBoolean;
6+
import org.enso.compiler.core.IR;
7+
import org.enso.interpreter.runtime.Module;
8+
9+
public final class ModuleIndexing {
10+
11+
/**
12+
* State of indexing encapsulating for a given IR
13+
*
14+
* @param isIndexed true, if IR has been already indexed. False otherwise
15+
* @param ir IR of a module that has been/needs to be indexed
16+
*/
17+
public record IndexState(boolean isIndexed, IR ir) {
18+
private IndexState toIndexed() {
19+
assert !isIndexed;
20+
return new IndexState(true, ir);
21+
}
22+
23+
private IndexState withIr(IR ir) {
24+
assert isIndexed;
25+
return new IndexState(true, ir);
26+
}
27+
}
28+
29+
private final ConcurrentMap<Module, IndexState> modules;
30+
31+
private ModuleIndexing() {
32+
this.modules = new ConcurrentHashMap<>();
33+
}
34+
35+
public static ModuleIndexing createInstance() {
36+
return new ModuleIndexing();
37+
}
38+
39+
/**
40+
* Finds `IndexState` corresponding to the module.
41+
*
42+
* @return IndexState corresponding to `module`, or null if it doesn't exist.
43+
*/
44+
public IndexState find(Module module) {
45+
return modules.get(module);
46+
}
47+
48+
/**
49+
* Get index state for a module or assigns a new one.
50+
*
51+
* @param module module for which lookup is performed
52+
* @param ir IR for which index is calculated, if new
53+
* @return index state assigned to the module, or a new one if absent
54+
*/
55+
public IndexState getOrCreateFresh(Module module, IR ir) {
56+
return modules.computeIfAbsent(module, m -> new IndexState(false, ir));
57+
}
58+
59+
/**
60+
* Attempts to update the index state for a module. If the provided state does not match the one
61+
* currently assigned to the module, no update is performed.
62+
*
63+
* @param state reference index state to be updated
64+
* @return true if the operation of updating the state was successful, false if the reference
65+
* state was not up-to-date.
66+
*/
67+
public boolean markAsIndexed(Module module, IndexState state) {
68+
AtomicBoolean updated = new AtomicBoolean(false);
69+
modules.compute(
70+
module,
71+
(k, v) -> {
72+
if (v == state) {
73+
updated.set(true);
74+
return state.toIndexed();
75+
} else {
76+
return v;
77+
}
78+
});
79+
return updated.get();
80+
}
81+
82+
/**
83+
* Attempts to update the index state for a module with a given IR. If the provided state does not
84+
* match the one currently assigned to the module, no update is performed.
85+
*
86+
* @param state reference index state to be updated
87+
* @param ir IR for which the index has been calculated
88+
* @return true if the operation of updating the state was successful, false if the reference
89+
* state was not up-to-date.
90+
*/
91+
public boolean updateState(Module module, IndexState state, IR ir) {
92+
AtomicBoolean updated = new AtomicBoolean(false);
93+
modules.compute(
94+
module,
95+
(k, v) -> {
96+
if (v == state) {
97+
updated.set(true);
98+
return state.withIr(ir);
99+
} else {
100+
return v;
101+
}
102+
});
103+
return updated.get();
104+
}
105+
106+
/** Clear index state for a provided module. */
107+
public void markIndexAsDirty(Module module) {
108+
modules.compute(module, (k, v) -> null);
109+
}
110+
}

engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/command/DeserializeLibrarySuggestionsCmd.scala

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ class DeserializeLibrarySuggestionsCmd(
2020
ctx: RuntimeContext,
2121
ec: ExecutionContext
2222
): Future[Unit] = {
23-
ctx.jobProcessor.runBackground(
24-
new DeserializeLibrarySuggestionsJob(request.libraryName)
25-
)
26-
Future.successful(())
23+
Future {
24+
ctx.jobProcessor.runBackground(
25+
new DeserializeLibrarySuggestionsJob(request.libraryName)
26+
)
27+
}
2728
}
2829
}

engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/command/RenameProjectCmd.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ class RenameProjectCmd(
5656
)
5757

5858
projectModules.foreach { module =>
59-
Module.fromCompilerModule(module).setIndexed(false)
59+
ctx.state.suggestions.markIndexAsDirty(
60+
Module.fromCompilerModule(module)
61+
)
6062
ctx.endpoint.sendToClient(
6163
Api.Response(
6264
Api.SuggestionsDatabaseModuleUpdateNotification(

engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/execution/CommandExecutionEngine.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class CommandExecutionEngine(interpreterContext: InterpreterContext)
4848
"Executing commands in a separate command pool"
4949
)
5050
interpreterContext.executionService.getContext
51-
.newCachedThreadPool("command-pool", false)
51+
.newCachedThreadPool("command-pool", 2, 10, 50, false)
5252
}
5353

5454
private val sequentialExecutionService =
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package org.enso.interpreter.instrument.execution
22

3-
/** The state of the runtime.
4-
*
5-
* @param pendingEdits the storage for pending file edits
6-
*/
7-
final class ExecutionState(
3+
/** The state of the runtime */
4+
final class ExecutionState {
5+
6+
/** The storage for pending file edits */
87
val pendingEdits: PendingEdits = new PendingFileEdits()
9-
)
8+
9+
val suggestions: ModuleIndexing = ModuleIndexing.createInstance()
10+
}

engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/execution/JobExecutionEngine.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,13 @@ final class JobExecutionEngine(
5050
context.newCachedThreadPool(
5151
"prioritized-job-pool",
5252
2,
53-
Integer.MAX_VALUE,
53+
4,
54+
50,
5455
false
5556
)
5657

5758
private val backgroundJobExecutor: ExecutorService =
58-
context.newFixedThreadPool(1, "background-job-pool", false)
59+
context.newCachedThreadPool("background-job-pool", 1, 4, 50, false)
5960

6061
private val runtimeContext =
6162
RuntimeContext(

engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/job/AnalyzeModuleInScopeJob.scala

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import org.enso.compiler.context.{
66
SuggestionBuilder,
77
SuggestionDiff
88
}
9+
import org.enso.interpreter.instrument.execution.ModuleIndexing.IndexState
910
import org.enso.interpreter.instrument.execution.RuntimeContext
1011
import org.enso.interpreter.runtime.Module
1112
import org.enso.polyglot.data.Tree
@@ -15,7 +16,7 @@ import org.enso.polyglot.{ModuleExports, Suggestion}
1516
import java.util.logging.Level
1617

1718
final class AnalyzeModuleInScopeJob(
18-
modules: Iterable[Module]
19+
modules: Iterable[(Module, IndexState, Boolean)]
1920
) extends BackgroundJob[Unit](AnalyzeModuleInScopeJob.Priority) {
2021

2122
private val exportsBuilder = new ExportsBuilder
@@ -27,7 +28,7 @@ final class AnalyzeModuleInScopeJob(
2728
// disable the suggestion updates and reduce the number of messages that
2829
// runtime sends.
2930
if (ctx.executionService.getContext.isProjectSuggestionsEnabled) {
30-
modules.foreach(analyzeModuleInScope)
31+
modules.foreach((analyzeModuleInScope _).tupled)
3132
ctx.endpoint.sendToClient(
3233
Api.Response(Api.AnalyzeModuleInScopeJobFinished())
3334
)
@@ -37,31 +38,43 @@ final class AnalyzeModuleInScopeJob(
3738
override def toString: String =
3839
s"AnalyzeModuleInScopeJob($modules)"
3940

40-
private def analyzeModuleInScope(module: Module)(implicit
41+
private def analyzeModuleInScope(
42+
module: Module,
43+
state: IndexState,
44+
hasSource: Boolean
45+
)(implicit
4146
ctx: RuntimeContext
4247
): Unit = {
43-
if (!module.isIndexed && module.getSource != null) {
48+
if (!state.isIndexed && hasSource) {
4449
ctx.executionService.getLogger
45-
.log(Level.FINEST, s"Analyzing module in scope ${module.getName}")
50+
.log(Level.FINEST, s"Analyzing module in scope {0}", module.getName)
4651
val moduleName = module.getName
4752
val newSuggestions =
4853
SuggestionBuilder(
4954
module.asCompilerModule(),
5055
ctx.executionService.getContext.getCompiler
5156
)
52-
.build(moduleName, module.getIr)
57+
.build(moduleName, state.ir)
5358
.filter(Suggestion.isGlobal)
5459
val prevExports = ModuleExports(moduleName.toString, Set())
55-
val newExports = exportsBuilder.build(module.getName, module.getIr)
60+
val newExports = exportsBuilder.build(module.getName, state.ir)
5661
val notification = Api.SuggestionsDatabaseModuleUpdateNotification(
5762
module = moduleName.toString,
5863
actions =
5964
Vector(Api.SuggestionsDatabaseAction.Clean(moduleName.toString)),
6065
exports = ModuleExportsDiff.compute(prevExports, newExports),
6166
updates = SuggestionDiff.compute(Tree.empty, newSuggestions)
6267
)
63-
sendModuleUpdate(notification)
64-
module.setIndexed(true)
68+
if (ctx.state.suggestions.markAsIndexed(module, state)) {
69+
sendModuleUpdate(notification)
70+
} else {
71+
ctx.executionService.getLogger
72+
.log(
73+
Level.FINEST,
74+
s"Calculated index for module in scope {0} is not up-to-date. Discarding",
75+
module.getName
76+
)
77+
}
6578
}
6679
}
6780

@@ -89,7 +102,9 @@ object AnalyzeModuleInScopeJob {
89102
* @param modules the list of modules to analyze
90103
* @return the [[AnalyzeModuleInScopeJob]]
91104
*/
92-
def apply(modules: Iterable[Module]): AnalyzeModuleInScopeJob =
105+
def apply(
106+
modules: Iterable[(Module, IndexState, Boolean)]
107+
): AnalyzeModuleInScopeJob =
93108
new AnalyzeModuleInScopeJob(modules)
94109

95110
private val Priority = 11

0 commit comments

Comments
 (0)