diff --git a/src/ScriptEngine/Machine/CodeStat/CodeStatProcessor.cs b/src/ScriptEngine/Machine/CodeStat/CodeStatProcessor.cs
index 15be6508f..4e9de3071 100644
--- a/src/ScriptEngine/Machine/CodeStat/CodeStatProcessor.cs
+++ b/src/ScriptEngine/Machine/CodeStat/CodeStatProcessor.cs
@@ -5,86 +5,137 @@ This Source Code Form is subject to the terms of the
at http://mozilla.org/MPL/2.0/.
----------------------------------------------------------*/
+using System;
+using System.Collections.Concurrent;
using System.Diagnostics;
-using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
namespace ScriptEngine.Machine
{
+ ///
+ /// Сборщик статистики исполнения кода. Его вызывают все потоки, где исполняется код,
+ /// в том числе фоновые задания: коллекции потокобезопасные, а время каждый поток меряет сам.
+ ///
public class CodeStatProcessor : ICodeStatCollector
{
- private Dictionary _codeStat = new Dictionary();
- private Dictionary _watchers = new Dictionary();
- private Stopwatch _activeStopwatch = null;
- private HashSet _preparedScripts = new HashSet();
+ private readonly ConcurrentDictionary _stats =
+ new ConcurrentDictionary();
+
+ private readonly ConcurrentDictionary _preparedScripts =
+ new ConcurrentDictionary();
+
+ // Точка, которую сейчас исполняет поток: ей идет время до перехода к следующей точке
+ private readonly ThreadLocal _activeEntry =
+ new ThreadLocal(() => new ActiveEntry());
+
+ // Делегат создается один раз: лямбда в вызове GetOrAdd выделяла бы память на каждую строку
+ private readonly Func _newStat;
+
+ // Отчет идет в порядке регистрации точек
+ private long _registrationOrder;
+
+ public CodeStatProcessor()
+ {
+ _newStat = _ => new EntryStat(Interlocked.Increment(ref _registrationOrder));
+ }
public bool IsPrepared(string ScriptFileName)
{
- return _preparedScripts.Contains(ScriptFileName);
+ return _preparedScripts.ContainsKey(ScriptFileName);
}
public void MarkEntryReached(CodeStatEntry entry, int count = 1)
{
- int oldValue = 0;
- _codeStat.TryGetValue(entry, out oldValue);
- _codeStat[entry] = oldValue + count;
-
+ var stat = _stats.GetOrAdd(entry, _newStat);
if (count == 0)
- {
- if (!_watchers.ContainsKey(entry))
- {
- _watchers.Add(entry, new Stopwatch());
- }
- }
- else
- {
- _activeStopwatch?.Stop();
- _activeStopwatch = _watchers[entry];
- _activeStopwatch.Start();
- }
+ return;
+
+ Interlocked.Add(ref stat.Count, count);
+ SwitchTo(stat);
}
public void MarkPrepared(string scriptFileName)
{
- _preparedScripts.Add(scriptFileName);
+ _preparedScripts.TryAdd(scriptFileName, true);
}
+ ///
+ /// Снимок статистики. Можно брать, пока код еще исполняется в других потоках.
+ ///
public CodeStatDataCollection GetStatData()
{
- CodeStatDataCollection data = new CodeStatDataCollection();
- foreach (var item in _codeStat)
+ var data = new CodeStatDataCollection();
+ foreach (var item in _stats.ToArray().OrderBy(x => x.Value.Order))
{
if (!IsPrepared(item.Key.ScriptFileName))
{
continue;
}
- data.Add(new CodeStatData(item.Key, _watchers[item.Key].ElapsedMilliseconds, item.Value));
+ data.Add(new CodeStatData(item.Key, item.Value.ElapsedMilliseconds, Volatile.Read(ref item.Value.Count)));
}
-
+
return data;
}
+ ///
+ /// Завершает замер времени в текущем потоке
+ ///
public void EndCodeStat()
{
- _activeStopwatch?.Stop();
+ StopCurrentWatch();
+ }
+
+ public void StopCurrentWatch()
+ {
+ SwitchTo(null);
}
public void StopWatch(CodeStatEntry entry)
{
- if (_watchers.ContainsKey(entry))
+ if (_stats.TryGetValue(entry, out var stat) && _activeEntry.Value.Stat == stat)
{
- _watchers[entry].Stop();
+ SwitchTo(null);
}
}
public void ResumeWatch(CodeStatEntry entry)
{
- _activeStopwatch?.Stop();
+ _stats.TryGetValue(entry, out var stat);
+ SwitchTo(stat);
+ }
+
+ private void SwitchTo(EntryStat next)
+ {
+ var active = _activeEntry.Value;
+ var now = Stopwatch.GetTimestamp();
+ active.Stat?.AddElapsed(now - active.Since);
+ active.Stat = next;
+ active.Since = now;
+ }
- if (_watchers.ContainsKey(entry))
+ private sealed class EntryStat
+ {
+ public readonly long Order;
+ public int Count;
+ private long _elapsedTicks;
+
+ public EntryStat(long order)
{
- _activeStopwatch = _watchers[entry];
- _activeStopwatch.Start();
+ Order = order;
}
+
+ public void AddElapsed(long ticks) => Interlocked.Add(ref _elapsedTicks, ticks);
+
+ // Так же, как Stopwatch.ElapsedMilliseconds
+ public long ElapsedMilliseconds =>
+ Stopwatch.GetElapsedTime(0, Interlocked.Read(ref _elapsedTicks)).Ticks / TimeSpan.TicksPerMillisecond;
+ }
+
+ private sealed class ActiveEntry
+ {
+ public EntryStat Stat;
+ public long Since;
}
}
}
diff --git a/src/ScriptEngine/Machine/CodeStat/ICodeStatCollector.cs b/src/ScriptEngine/Machine/CodeStat/ICodeStatCollector.cs
index 79e093d5a..26a3fdb00 100644
--- a/src/ScriptEngine/Machine/CodeStat/ICodeStatCollector.cs
+++ b/src/ScriptEngine/Machine/CodeStat/ICodeStatCollector.cs
@@ -15,5 +15,11 @@ public interface ICodeStatCollector
void StopWatch(CodeStatEntry entry);
void ResumeWatch(CodeStatEntry entry);
+
+ ///
+ /// Текущий поток закончил исполнять код: время до следующего исполнения на нем
+ /// не относится к последней достигнутой точке
+ ///
+ void StopCurrentWatch();
}
}
diff --git a/src/ScriptEngine/Machine/StackMachineExecutor.cs b/src/ScriptEngine/Machine/StackMachineExecutor.cs
index fb6aa2614..09152f795 100644
--- a/src/ScriptEngine/Machine/StackMachineExecutor.cs
+++ b/src/ScriptEngine/Machine/StackMachineExecutor.cs
@@ -45,6 +45,10 @@ public void BeforeProcessStart(IBslProcess process)
public void AfterProcessExit(IBslProcess process)
{
+ // Поток фонового задания или запроса веб-сервера возвращается в пул,
+ // и время простоя не должно уйти последней исполненной строке
+ process.Services.TryResolve()?.StopCurrentWatch();
+
var debugger = process.Services.TryResolve();
if (debugger?.IsEnabled != true)
return;
diff --git a/src/ScriptEngine/Machine/TypeFactory.cs b/src/ScriptEngine/Machine/TypeFactory.cs
index 93c2401f6..ff9ee620c 100644
--- a/src/ScriptEngine/Machine/TypeFactory.cs
+++ b/src/ScriptEngine/Machine/TypeFactory.cs
@@ -7,6 +7,7 @@ This Source Code Form is subject to the terms of the
using ScriptEngine.Machine.Contexts;
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
@@ -25,7 +26,8 @@ public class TypeFactory : ITypeFactory
{
private readonly TypeDescriptor _systemType;
- private Dictionary _constructorsCache = new Dictionary();
+ // Фабрика одна на тип, а объекты создаются из разных потоков
+ private readonly ConcurrentDictionary _constructorsCache = new ConcurrentDictionary();
private static readonly Refl.MethodInfo CopyMethod = typeof(TypeFactory)
.GetMethod(nameof(CaptureVariantArgs), Refl.BindingFlags.Static | Refl.BindingFlags.InvokeMethod | Refl.BindingFlags.NonPublic);
diff --git a/src/ScriptEngine/Types/TypeFactoryCache.cs b/src/ScriptEngine/Types/TypeFactoryCache.cs
index aba6d3600..bf50ed041 100644
--- a/src/ScriptEngine/Types/TypeFactoryCache.cs
+++ b/src/ScriptEngine/Types/TypeFactoryCache.cs
@@ -5,7 +5,7 @@ This Source Code Form is subject to the terms of the
at http://mozilla.org/MPL/2.0/.
----------------------------------------------------------*/
-using System.Collections.Generic;
+using System.Collections.Concurrent;
using OneScript.Types;
using ScriptEngine.Machine;
@@ -13,17 +13,12 @@ namespace ScriptEngine.Types
{
public class TypeFactoryCache
{
- private readonly Dictionary _factories = new Dictionary();
+ // Объекты создаются из всех потоков, где исполняется код, в том числе из фоновых заданий
+ private readonly ConcurrentDictionary _factories = new ConcurrentDictionary();
public TypeFactory GetFactoryFor(TypeDescriptor type)
{
- if (!_factories.TryGetValue(type, out var factory))
- {
- factory = new TypeFactory(type);
- _factories[type] = factory;
- }
-
- return factory;
+ return _factories.GetOrAdd(type, t => new TypeFactory(t));
}
}
}
\ No newline at end of file
diff --git a/src/Tests/OneScript.Core.Tests/CodeStatProcessorTests.cs b/src/Tests/OneScript.Core.Tests/CodeStatProcessorTests.cs
new file mode 100644
index 000000000..a2f80e944
--- /dev/null
+++ b/src/Tests/OneScript.Core.Tests/CodeStatProcessorTests.cs
@@ -0,0 +1,334 @@
+/*----------------------------------------------------------
+This Source Code Form is subject to the terms of the
+Mozilla Public License, v.2.0. If a copy of the MPL
+was not distributed with this file, You can obtain one
+at http://mozilla.org/MPL/2.0/.
+----------------------------------------------------------*/
+
+using System;
+using System.Collections.Concurrent;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using FluentAssertions;
+using OneScript.StandardLibrary;
+using ScriptEngine.HostedScript;
+using ScriptEngine.HostedScript.Extensions;
+using ScriptEngine.Hosting;
+using ScriptEngine.Machine;
+using Xunit;
+
+namespace OneScript.Core.Tests
+{
+ public class CodeStatProcessorTests
+ {
+ private const string ModuleName = "module.os";
+
+ [Fact]
+ public void CountsFromManyThreadsAddUp()
+ {
+ const int threadsCount = 16;
+ const int iterations = 20000;
+ var collector = new CodeStatProcessor();
+ var entries = Enumerable.Range(1, 50).Select(i => new CodeStatEntry(ModuleName, "Метод", i)).ToArray();
+
+ RunInParallel(threadsCount, () =>
+ {
+ // Все потоки впервые исполняют один и тот же модуль и регистрируют одни и те же точки
+ foreach (var entry in entries)
+ {
+ collector.MarkEntryReached(entry, count: 0);
+ }
+ collector.MarkPrepared(ModuleName);
+
+ for (var i = 0; i < iterations; i++)
+ {
+ collector.MarkEntryReached(entries[i % entries.Length]);
+ }
+ });
+
+ collector.EndCodeStat();
+ var data = collector.GetStatData();
+
+ data.Should().HaveCount(entries.Length);
+ data.Select(x => x.ExecutionCount).Should()
+ .AllBeEquivalentTo(threadsCount * iterations / entries.Length);
+ }
+
+ [Fact]
+ public void StatDataCanBeTakenWhileThreadsAreCounting()
+ {
+ var collector = new CodeStatProcessor();
+ var stop = 0;
+ var exceptions = new ConcurrentQueue();
+
+ var workers = Enumerable.Range(0, 8).Select(t => new Thread(() =>
+ {
+ try
+ {
+ var line = 0;
+ while (Volatile.Read(ref stop) == 0)
+ {
+ // Новые точки появляются, пока уже зарегистрированные считаются
+ var entry = new CodeStatEntry(ModuleName, "Метод" + t, line++ % 1000);
+ collector.MarkEntryReached(entry, count: 0);
+ collector.MarkEntryReached(entry);
+ }
+ }
+ catch (Exception e)
+ {
+ exceptions.Enqueue(e);
+ }
+ })).ToArray();
+
+ collector.MarkPrepared(ModuleName);
+ foreach (var worker in workers)
+ {
+ worker.Start();
+ }
+
+ try
+ {
+ for (var i = 0; i < 200; i++)
+ {
+ collector.GetStatData();
+ collector.EndCodeStat();
+ }
+ }
+ finally
+ {
+ Volatile.Write(ref stop, 1);
+ foreach (var worker in workers)
+ {
+ worker.Join();
+ }
+ }
+
+ exceptions.Should().BeEmpty();
+ collector.GetStatData().Should().HaveCount(8 * 1000);
+ }
+
+ [Fact]
+ public void TimeIsAccountedToTheLastReachedEntry()
+ {
+ var collector = new CodeStatProcessor();
+ var first = new CodeStatEntry(ModuleName, "Метод", 1);
+ var second = new CodeStatEntry(ModuleName, "Метод", 2);
+ collector.MarkEntryReached(first, count: 0);
+ collector.MarkEntryReached(second, count: 0);
+ collector.MarkPrepared(ModuleName);
+
+ collector.MarkEntryReached(first);
+ Thread.Sleep(100);
+ collector.MarkEntryReached(second);
+ collector.MarkEntryReached(first);
+ Thread.Sleep(100);
+ collector.EndCodeStat();
+
+ var data = collector.GetStatData().ToDictionary(x => x.Entry.LineNumber);
+ data[1].ExecutionCount.Should().Be(2);
+ data[1].TimeElapsed.Should().BeGreaterOrEqualTo(190);
+ data[2].ExecutionCount.Should().Be(1);
+ data[2].TimeElapsed.Should().BeLessThan(50);
+ }
+
+ [Fact]
+ public void StoppedEntryDoesNotAccumulateTime()
+ {
+ var collector = new CodeStatProcessor();
+ var entry = new CodeStatEntry(ModuleName, "Метод", 1);
+ collector.MarkEntryReached(entry, count: 0);
+ collector.MarkPrepared(ModuleName);
+
+ collector.MarkEntryReached(entry);
+ collector.StopWatch(entry);
+ Thread.Sleep(100);
+ collector.EndCodeStat();
+
+ collector.GetStatData().Single().TimeElapsed.Should().BeLessThan(50);
+ }
+
+ [Fact]
+ public void StatDataKeepsRegistrationOrder()
+ {
+ var collector = new CodeStatProcessor();
+ var entries = Enumerable.Range(1, 100)
+ .Select(i => new CodeStatEntry("module" + (i % 7) + ".os", "Метод" + (i % 3), i))
+ .ToArray();
+
+ foreach (var entry in entries)
+ {
+ collector.MarkEntryReached(entry, count: 0);
+ collector.MarkPrepared(entry.ScriptFileName);
+ }
+
+ collector.GetStatData().Select(x => x.Entry).Should().Equal(entries);
+ }
+
+ [Fact]
+ public void UnregisteredEntryDoesNotBreakExecution()
+ {
+ var collector = new CodeStatProcessor();
+ var entry = new CodeStatEntry(ModuleName, "Метод", 1);
+
+ Action act = () => collector.MarkEntryReached(entry);
+
+ act.Should().NotThrow();
+ }
+
+ [Fact]
+ public void BackgroundTasksExecuteSameModulesUnderCodeStat()
+ {
+ const int tasksCount = 16;
+ const int classesCount = 10;
+ const int repeats = 200;
+
+ var dir = Path.Combine(Path.GetTempPath(), "codestat-" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(dir);
+ try
+ {
+ for (var i = 0; i < classesCount; i++)
+ {
+ File.WriteAllText(Path.Combine(dir, $"Класс{i}.os"), ClassModule);
+ }
+
+ var collector = new CodeStatProcessor();
+ var engine = CreateEngine(collector);
+
+ var mainModule = MainModule
+ .Replace("%КАТАЛОГ%", dir)
+ .Replace("%ЗАДАНИЙ%", tasksCount.ToString())
+ .Replace("%КЛАССОВ%", classesCount.ToString())
+ .Replace("%ПОВТОРОВ%", repeats.ToString());
+
+ RunScript(engine, mainModule);
+
+ collector.EndCodeStat();
+ var data = collector.GetStatData();
+ for (var i = 0; i < classesCount; i++)
+ {
+ var fileName = $"Класс{i}.os";
+ // Строка тела цикла в методе Посчитать
+ data.Single(x => x.Entry.ScriptFileName.EndsWith(fileName) && x.Entry.LineNumber == 5)
+ .ExecutionCount.Should().Be(tasksCount * repeats, fileName);
+ }
+ }
+ finally
+ {
+ Directory.Delete(dir, true);
+ }
+ }
+
+ [Fact]
+ public void IdleTimeAfterProcessExitIsNotAccounted()
+ {
+ var collector = new CodeStatProcessor();
+ var engine = CreateEngine(collector);
+
+ // Как поток пула: процесс завершился, поток простаивает, потом исполняет следующий процесс
+ RunScript(engine, "А = 1;");
+ Thread.Sleep(200);
+ RunScript(engine, "Б = 2;");
+ collector.EndCodeStat();
+
+ collector.GetStatData().Select(x => x.TimeElapsed).Should().OnlyContain(x => x < 100);
+ }
+
+ // Движок собирается так же, как в oscript с -codestat
+ private static HostedScriptEngine CreateEngine(ICodeStatCollector collector)
+ {
+ var builder = DefaultEngineBuilder.Create()
+ .SetDefaultOptions()
+ .UseImports()
+ .UseDefaultHosting()
+ .SetupEnvironment(e => e.AddStandardLibrary());
+ builder.Services.RegisterSingleton(collector);
+ var engine = new HostedScriptEngine(builder.Build());
+ engine.Initialize();
+ return engine;
+ }
+
+ private static void RunScript(HostedScriptEngine engine, string code)
+ {
+ engine.Engine.AttachedScriptsFactory.LoadFromString(
+ engine.GetCompilerService(), code, engine.Engine.NewProcess());
+ }
+
+ private const string ClassModule =
+ "Перем Сумма;\n" +
+ "\n" +
+ "Процедура Посчитать(Повторов) Экспорт\n" +
+ "\tДля Н = 1 По Повторов Цикл\n" +
+ "\t\tСумма = Сумма + 1;\n" +
+ "\tКонецЦикла;\n" +
+ "КонецПроцедуры\n" +
+ "\n" +
+ "Сумма = 0;\n";
+
+ private const string MainModule =
+ "Перем Старт;\n" +
+ "\n" +
+ "Процедура Работа(НомерЗадания) Экспорт\n" +
+ "\tПока Не Старт Цикл\n" +
+ "\tКонецЦикла;\n" +
+ "\tДля Н = 0 По %КЛАССОВ% - 1 Цикл\n" +
+ "\t\tОбъект = Новый(\"Класс\" + Н);\n" +
+ "\t\tОбъект.Посчитать(%ПОВТОРОВ%);\n" +
+ "\t\tЗагрузитьСценарийИзСтроки(\"Перем Х; Х = \" + Формат(НомерЗадания * 100 + Н, \"ЧГ=\") + \";\");\n" +
+ "\tКонецЦикла;\n" +
+ "КонецПроцедуры\n" +
+ "\n" +
+ "Для Н = 0 По %КЛАССОВ% - 1 Цикл\n" +
+ "\tПодключитьСценарий(ОбъединитьПути(\"%КАТАЛОГ%\", \"Класс\" + Н + \".os\"), \"Класс\" + Н);\n" +
+ "КонецЦикла;\n" +
+ "\n" +
+ "Старт = Ложь;\n" +
+ "Задания = Новый Массив;\n" +
+ "Для Н = 1 По %ЗАДАНИЙ% Цикл\n" +
+ "\tПараметры = Новый Массив;\n" +
+ "\tПараметры.Добавить(Н);\n" +
+ "\tЗадания.Добавить(ФоновыеЗадания.Выполнить(ЭтотОбъект, \"Работа\", Параметры));\n" +
+ "КонецЦикла;\n" +
+ "Старт = Истина;\n" +
+ "ФоновыеЗадания.ОжидатьВсе(Задания);\n" +
+ "\n" +
+ "Ошибки = \"\";\n" +
+ "Для Каждого Задание Из Задания Цикл\n" +
+ "\tЕсли Задание.ИнформацияОбОшибке <> Неопределено Тогда\n" +
+ "\t\tОшибки = Ошибки + Задание.ИнформацияОбОшибке.ПодробноеОписаниеОшибки() + Символы.ПС;\n" +
+ "\tКонецЕсли;\n" +
+ "КонецЦикла;\n" +
+ "Если Ошибки <> \"\" Тогда\n" +
+ "\tВызватьИсключение Ошибки;\n" +
+ "КонецЕсли;\n";
+
+ private static void RunInParallel(int threadsCount, Action action)
+ {
+ var exceptions = new ConcurrentQueue();
+ using var barrier = new Barrier(threadsCount);
+ var threads = Enumerable.Range(0, threadsCount).Select(_ => new Thread(() =>
+ {
+ try
+ {
+ barrier.SignalAndWait();
+ action();
+ }
+ catch (Exception e)
+ {
+ exceptions.Enqueue(e);
+ }
+ })).ToArray();
+
+ foreach (var thread in threads)
+ {
+ thread.Start();
+ }
+ foreach (var thread in threads)
+ {
+ thread.Join();
+ }
+
+ exceptions.Should().BeEmpty();
+ }
+ }
+}