Skip to content

Commit cdb8832

Browse files
Improve compilation, no longer load assembly to app domain on every plugin compilation, reduced memory allocations
1 parent a2dd933 commit cdb8832

4 files changed

Lines changed: 210 additions & 176 deletions

File tree

src/CompilableFile.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public class CompilableFile
3737

3838
private Core.Libraries.Timer.TimerInstance timeoutTimer;
3939

40-
public byte[] ScriptSource => ScriptEncoding.GetBytes(string.Join(Environment.NewLine, ScriptLines));
40+
public byte[]? ScriptSource => ScriptEncoding.GetBytes(ScriptLines.JoinValues(Environment.NewLine));
4141

4242
public CompilableFile(CSharpExtension extension, CSharpPluginLoader loader, string directory, string name)
4343
{

src/Compilation.cs

Lines changed: 110 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
extern alias References;
12
using Oxide.Core;
23
using Oxide.Core.Logging;
34
using Oxide.Logging;
@@ -10,12 +11,14 @@
1011
using System.Threading;
1112
using Oxide.CSharp.Common;
1213
using Oxide.CSharp.CompilerStream;
14+
using Oxide.Pooling;
15+
using References::Mono.Cecil;
1316

1417
namespace Oxide.Plugins
1518
{
1619
internal class Compilation
1720
{
18-
public static Compilation Current;
21+
public static Compilation? Current;
1922

2023
internal int id;
2124
internal string name;
@@ -29,7 +32,6 @@ internal class Compilation
2932
internal CompiledAssembly compiledAssembly;
3033
internal float duration => endedAt - startedAt;
3134

32-
private string includePath;
3335
private string[] extensionNames;
3436

3537
internal Compilation(int id, Action<Compilation> callback, List<CompilablePlugin> plugins)
@@ -38,18 +40,16 @@ internal Compilation(int id, Action<Compilation> callback, List<CompilablePlugin
3840
this.callback = callback;
3941
queuedPlugins = new ConcurrentHashSet<CompilablePlugin>(plugins);
4042

41-
if (Current == null)
42-
{
43-
Current = this;
44-
}
43+
Current ??= this;
4544

46-
foreach (CompilablePlugin plugin in plugins)
45+
int pluginCount = plugins.Count;
46+
for (int i = 0; i < pluginCount; i++)
4747
{
48+
CompilablePlugin plugin = plugins[i];
4849
plugin.CompilerErrors.Clear();
4950
plugin.OnCompilationStarted();
5051
}
5152

52-
includePath = Path.Combine(Interface.Oxide.PluginDirectory, "include");
5353
extensionNames = Interface.Oxide.GetAllExtensions().Select(ext => ext.Name).ToArray();
5454
}
5555

@@ -58,7 +58,7 @@ internal void Started()
5858
name = (plugins.Count < 2 ? plugins.First().Name : "plugins_") + Math.Round(Interface.Oxide.Now * 10000000f) + ".dll";
5959
}
6060

61-
internal void Completed(byte[] rawAssembly = null, byte[] symbols = null)
61+
internal void Completed(byte[]? rawAssembly = null, byte[]? symbols = null)
6262
{
6363
endedAt = Interface.Oxide.Now;
6464
if (plugins.Count > 0 && rawAssembly != null)
@@ -144,56 +144,66 @@ internal void Prepare(Action callback)
144144

145145
Interface.Oxide.RootLogger.WriteDebug(LogType.Info, LogEvent.Compile, "CSharp", $"Preparing compilation");
146146

147-
List<CompilablePlugin> pluginsToAdd = new List<CompilablePlugin>();
148-
149-
while (queuedPlugins.TryDequeue(out CompilablePlugin plugin))
147+
List<CompilablePlugin> pluginsToAdd = PoolFactory<List<CompilablePlugin>>.Shared.Take();
148+
try
150149
{
151-
if (Current == null)
152-
{
153-
Current = this;
154-
}
155150

156-
if (!CacheScriptLines(plugin) || plugin.ScriptLines.Length < 1)
151+
while (queuedPlugins.TryDequeue(out CompilablePlugin plugin))
157152
{
158-
plugin.References.Clear();
159-
plugin.IncludePaths.Clear();
160-
plugin.Requires.Clear();
161-
Interface.Oxide.RootLogger.WriteDebug(LogType.Error, LogEvent.Compile, "CSharp", $"Script file is empty: {plugin.Name}");
162-
RemovePlugin(plugin);
163-
}
153+
Current ??= this;
164154

165-
if (!pluginsToAdd.Contains(plugin))
166-
{
167-
pluginsToAdd.Add(plugin);
155+
if (!CacheScriptLines(plugin) || plugin.ScriptLines.Length < 1)
156+
{
157+
plugin.References.Clear();
158+
plugin.IncludePaths.Clear();
159+
plugin.Requires.Clear();
160+
Interface.Oxide.RootLogger.WriteDebug(LogType.Error, LogEvent.Compile, "CSharp", $"Script file is empty: {plugin.Name}");
161+
RemovePlugin(plugin);
162+
}
168163

169-
PreparseScript(plugin);
170-
ResolveReferences(plugin);
171-
}
172-
else
173-
{
174-
Interface.Oxide.RootLogger.WriteDebug(LogType.Error, LogEvent.Compile, "CSharp", $"Plugin is already part of the compilation: {plugin.Name}");
175-
}
164+
if (!pluginsToAdd.Contains(plugin))
165+
{
166+
pluginsToAdd.Add(plugin);
176167

177-
CacheModifiedScripts();
168+
PreparseScript(plugin);
178169

179-
// We don't want the main thread to be able to add more plugins which could be missed
180-
if (queuedPlugins.Count == 0 && Current == this)
181-
{
182-
Current = null;
170+
ResolveReferences(plugin);
171+
}
172+
else
173+
{
174+
Interface.Oxide.RootLogger.WriteDebug(LogType.Error, LogEvent.Compile, "CSharp", $"Plugin is already part of the compilation: {plugin.Name}");
175+
}
176+
177+
CacheModifiedScripts();
178+
179+
// We don't want the main thread to be able to add more plugins which could be missed
180+
if (queuedPlugins.Count == 0 && Current == this)
181+
{
182+
Current = null;
183+
}
183184
}
184-
}
185185

186-
pluginsToAdd.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal));
186+
pluginsToAdd.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal));
187187

188-
foreach (CompilablePlugin plugin in pluginsToAdd)
189-
{
190-
if (!plugins.Add(plugin))
188+
int pluginCount = pluginsToAdd.Count;
189+
for (int i = 0; i < pluginCount; i++)
191190
{
192-
Interface.Oxide.RootLogger.WriteDebug(LogType.Error, LogEvent.Compile, "CSharp", $"Failed to add plugin to compilation: {plugin.Name}");
193-
continue;
194-
}
191+
CompilablePlugin plugin = pluginsToAdd[i];
192+
if (!plugins.Add(plugin))
193+
{
194+
Interface.Oxide.RootLogger.WriteDebug(LogType.Error, LogEvent.Compile, "CSharp",
195+
$"Failed to add plugin to compilation: {plugin.Name}");
196+
continue;
197+
}
195198

196-
Interface.Oxide.RootLogger.WriteDebug(LogType.Info, LogEvent.Compile, "CSharp", $"Added plugin to compilation: {plugin.Name}");
199+
Interface.Oxide.RootLogger.WriteDebug(LogType.Info, LogEvent.Compile, "CSharp",
200+
$"Added plugin to compilation: {plugin.Name}");
201+
}
202+
}
203+
finally
204+
{
205+
pluginsToAdd.Clear();
206+
PoolFactory<List<CompilablePlugin>>.Shared.Return(pluginsToAdd);
197207
}
198208

199209
Interface.Oxide.RootLogger.WriteDebug(LogType.Info, LogEvent.Compile, "CSharp", $"Done preparing compilation: {plugins.Select(p => p.Name).ToSentence()}");
@@ -214,11 +224,11 @@ private void PreparseScript(CompilablePlugin plugin)
214224
plugin.Requires.Clear();
215225

216226
bool parsingNamespace = false;
217-
for (int i = 0; i < plugin.ScriptLines.Length; i++)
227+
int scriptLineCount = plugin.ScriptLines.Length;
228+
for (int i = 0; i < scriptLineCount; i++)
218229
{
219230
string line = plugin.ScriptLines[i].Trim();
220-
221-
if (line.IndexOf("namespace uMod.Plugins", StringComparison.InvariantCultureIgnoreCase) >= 0)
231+
if (line.IndexOf(Constants.UmodNamespace, StringComparison.InvariantCultureIgnoreCase) >= 0)
222232
{
223233
Interface.Oxide.LogError($"Plugin {plugin.ScriptName}.cs is a uMod plugin, not an Oxide plugin. Please downgrade to the Oxide version if available.");
224234
plugin.CompilerErrors.Add($"Plugin {plugin.ScriptName}.cs is a uMod plugin, not an Oxide plugin. Please downgrade to the Oxide version if available.");
@@ -234,16 +244,8 @@ private void PreparseScript(CompilablePlugin plugin)
234244
Match match;
235245
if (parsingNamespace)
236246
{
237-
// Skip blank lines and opening brace at the top of the namespace block
238-
match = Constants.BlankLineRegex.Match(line);
239-
if (match.Success)
240-
{
241-
continue;
242-
}
243-
244-
// Skip class custom attributes
245-
match = Constants.CustomAttributeRegex.Match(line);
246-
if (match.Success)
247+
// Skip opening brace at the top of the namespace block & class custom attributes
248+
if (line.StartsWith("[") || line.StartsWith("{"))
247249
{
248250
continue;
249251
}
@@ -272,7 +274,7 @@ private void PreparseScript(CompilablePlugin plugin)
272274
{
273275
string dependencyName = match.Groups[1].Value;
274276
plugin.Requires.Add(dependencyName);
275-
if (!File.Exists(Path.Combine(plugin.Directory, dependencyName + ".cs")))
277+
if (!File.Exists(Path.Combine(plugin.Directory, $"{dependencyName}.cs")))
276278
{
277279
Interface.Oxide.LogError($"{plugin.Name} plugin requires missing dependency: {dependencyName}");
278280
plugin.CompilerErrors.Add($"Missing dependency: {dependencyName}");
@@ -323,8 +325,7 @@ private void PreparseScript(CompilablePlugin plugin)
323325
}
324326

325327
// Start parsing the Oxide.Plugins namespace contents
326-
match = Constants.NamespaceRegex.Match(line);
327-
if (match.Success)
328+
if (line.IndexOf(Constants.OxideNamespace, StringComparison.InvariantCultureIgnoreCase) >= 0)
328329
{
329330
parsingNamespace = true;
330331
}
@@ -348,9 +349,9 @@ private void ResolveReferences(CompilablePlugin plugin)
348349
continue;
349350
}
350351

351-
if (Directory.Exists(includePath))
352+
if (Directory.Exists(Constants.IncludePath))
352353
{
353-
string includeFilePath = Path.Combine(includePath, $"Ext.{name}.cs");
354+
string includeFilePath = Path.Combine(Constants.IncludePath, $"Ext.{name}.cs");
354355
if (File.Exists(includeFilePath))
355356
{
356357
plugin.IncludePaths.Add(includeFilePath);
@@ -406,25 +407,24 @@ private void AddReference(CompilablePlugin plugin, string assemblyNameString)
406407
return;
407408
}
408409

409-
Assembly assembly;
410-
try
411-
{
412-
assembly = Assembly.Load(assemblyNameString);
413-
}
414-
catch (FileNotFoundException)
410+
using AssemblyDefinition? assemblyDefinition = AssemblyDefinition.ReadAssembly(path);
411+
if (assemblyDefinition == null)
415412
{
416413
Interface.Oxide.LogError($"Assembly referenced by {plugin.Name} plugin is invalid: {assemblyNameString}.dll");
417414
plugin.CompilerErrors.Add($"Referenced assembly is invalid: {assemblyNameString}");
418415
RemovePlugin(plugin);
419416
return;
420417
}
421418

422-
AssemblyName assemblyName = assembly.GetName();
423-
AddReference(plugin, assemblyName, $"{assemblyName.Name}.dll");
419+
string assemblyName = assemblyDefinition.Name.Name;
420+
421+
AddReference(plugin, assemblyName, $"{assemblyName}.dll");
424422

425423
// Include references made by the referenced assembly
426-
foreach (AssemblyName reference in assembly.GetReferencedAssemblies())
424+
int referenceCount = assemblyDefinition.MainModule.AssemblyReferences.Count;
425+
for (int i = 0; i < referenceCount; i++)
427426
{
427+
AssemblyNameReference reference = assemblyDefinition.MainModule.AssemblyReferences[i];
428428
// TODO: Fix Oxide.References to avoid these and other dependency conflicts
429429
if (reference.Name.StartsWith("Newtonsoft.Json") || reference.Name.StartsWith("Rust.Workshop"))
430430
{
@@ -435,25 +435,25 @@ private void AddReference(CompilablePlugin plugin, string assemblyNameString)
435435
string referencePath = Path.Combine(Interface.Oxide.ExtensionDirectory, referenceString);
436436
if (!File.Exists(referencePath))
437437
{
438-
Interface.Oxide.LogWarning($"Reference {reference.Name}.dll from {assembly.GetName().Name}.dll not found");
438+
Interface.Oxide.LogWarning($"Reference {reference.Name}.dll from {assemblyName}.dll not found");
439439
continue;
440440
}
441441

442-
AddReference(plugin, reference, referenceString);
442+
AddReference(plugin, reference.Name, referenceString);
443443
}
444444
}
445445

446-
private void AddReference(CompilablePlugin plugin, AssemblyName reference, string referenceString)
446+
private void AddReference(CompilablePlugin plugin, string reference, string referenceString)
447447
{
448448
if (!references.ContainsKey(referenceString))
449449
{
450450
Interface.Oxide.RootLogger.WriteDebug(LogType.Info, LogEvent.Compile, "CSharp",
451-
$"{reference.Name} has been added as a reference");
451+
$"{reference} has been added as a reference");
452452

453453
references[referenceString] = CompilerFile.CachedReadFile(Interface.Oxide.ExtensionDirectory, referenceString);
454454
}
455455

456-
plugin.References.Add(reference.Name);
456+
plugin.References.Add(reference);
457457
}
458458

459459
private bool CacheScriptLines(CompilablePlugin plugin)
@@ -472,26 +472,34 @@ private bool CacheScriptLines(CompilablePlugin plugin)
472472
}
473473

474474
plugin.CheckLastModificationTime();
475-
if (plugin.LastCachedScriptAt != plugin.LastModifiedAt)
475+
if (plugin.LastCachedScriptAt == plugin.LastModifiedAt)
476476
{
477-
using (StreamReader reader = File.OpenText(plugin.ScriptPath))
478-
{
479-
List<string> lines = new List<string>();
480-
while (!reader.EndOfStream)
481-
{
482-
lines.Add(reader.ReadLine());
483-
}
477+
return true;
478+
}
484479

485-
plugin.ScriptLines = lines.ToArray();
486-
plugin.ScriptEncoding = reader.CurrentEncoding;
480+
using StreamReader streamReader = File.OpenText(plugin.ScriptPath);
481+
List<string> lines = PoolFactory<List<string>>.Shared.Take();
482+
try
483+
{
484+
while (!streamReader.EndOfStream)
485+
{
486+
lines.Add(streamReader.ReadLine());
487487
}
488488

489+
plugin.ScriptLines = lines.ToArray();
490+
plugin.ScriptEncoding = streamReader.CurrentEncoding;
491+
489492
plugin.LastCachedScriptAt = plugin.LastModifiedAt;
490493
if (plugins.Remove(plugin))
491494
{
492495
queuedPlugins.Add(plugin);
493496
}
494497
}
498+
finally
499+
{
500+
lines.Clear();
501+
PoolFactory<List<string>>.Shared.Return(lines);
502+
}
495503

496504
return true;
497505
}
@@ -519,8 +527,10 @@ private void CacheModifiedScripts()
519527
return;
520528
}
521529

522-
foreach (CompilablePlugin plugin in modifiedPlugins)
530+
int modifiedPluginCount = modifiedPlugins.Length;
531+
for (int i = 0; i < modifiedPluginCount; i++)
523532
{
533+
CompilablePlugin plugin = modifiedPlugins[i];
524534
CacheScriptLines(plugin);
525535
}
526536

@@ -540,12 +550,19 @@ private void RemovePlugin(CompilablePlugin plugin)
540550
plugin.OnCompilationFailed();
541551

542552
// Remove plugins which are required by this plugin if they are only being compiled for this requirement
543-
foreach (CompilablePlugin requiredPlugin in plugins.Where(pl => !pl.IsCompilationNeeded && plugin.Requires.Contains(pl.Name)).ToArray())
553+
CompilablePlugin[] requiredPlugins = plugins.Where(pl =>
554+
!pl.IsCompilationNeeded && plugin.Requires.Contains(pl.Name)).ToArray();
555+
556+
int requiredPluginCount = requiredPlugins.Length;
557+
for (int i = 0; i < requiredPluginCount; i++)
544558
{
545-
if (!plugins.Any(pl => pl.Requires.Contains(requiredPlugin.Name)))
559+
CompilablePlugin requiredPlugin = requiredPlugins[i];
560+
if (plugins.Any(pl => pl.Requires.Contains(requiredPlugin.Name)))
546561
{
547-
RemovePlugin(requiredPlugin);
562+
continue;
548563
}
564+
565+
RemovePlugin(requiredPlugin);
549566
}
550567
}
551568
}

0 commit comments

Comments
 (0)