From 659d6735d3d25097919b8c1bf26130a98ce4b9e6 Mon Sep 17 00:00:00 2001 From: Sergey Batanov Date: Wed, 2 Sep 2026 21:44:16 +0300 Subject: [PATCH 1/6] =?UTF-8?q?=D0=A3=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D1=8F?= =?UTF-8?q?=D0=B5=D0=BC=D1=8B=D0=B9=20=D1=81=D0=B1=D0=BE=D1=80=20=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D1=82=D0=B8=D1=81=D1=82=D0=B8=D0=BA=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CodeStatisticsCollector.cs | 105 ++++++ .../CodeStatisticsContext.cs | 42 +++ .../HostedScriptEngine.cs | 3 + .../Machine/CodeStat/CodeStatHub.cs | 148 +++++++++ .../Machine/CodeStat/CodeStatProcessor.cs | 13 +- .../OneScript.Core.Tests/CodeStatHubTests.cs | 124 +++++++ src/oscript/BehaviorSelector.cs | 8 +- src/oscript/ExecuteScriptBehavior.cs | 19 +- src/oscript/ShowUsageBehavior.cs | 2 +- tests/codestat-api.os | 312 ++++++++++++++++++ tests/process.os | 2 +- 11 files changed, 763 insertions(+), 15 deletions(-) create mode 100644 src/OneScript.StandardLibrary/CodeStatisticsCollector.cs create mode 100644 src/OneScript.StandardLibrary/CodeStatisticsContext.cs create mode 100644 src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs create mode 100644 src/Tests/OneScript.Core.Tests/CodeStatHubTests.cs create mode 100644 tests/codestat-api.os diff --git a/src/OneScript.StandardLibrary/CodeStatisticsCollector.cs b/src/OneScript.StandardLibrary/CodeStatisticsCollector.cs new file mode 100644 index 000000000..992adef68 --- /dev/null +++ b/src/OneScript.StandardLibrary/CodeStatisticsCollector.cs @@ -0,0 +1,105 @@ +/*---------------------------------------------------------- +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.Linq; +using OneScript.Contexts; +using OneScript.Exceptions; +using OneScript.StandardLibrary.Collections.ValueTable; +using ScriptEngine.Machine; +using ScriptEngine.Machine.Contexts; + +namespace OneScript.StandardLibrary +{ + /// + /// Сессия сбора статистики исполнения кода. Создаётся через СборСтатистики.НачатьСбор(). + /// + [ContextClass("СборщикСтатистикиКода", "CodeStatisticsCollector")] + public sealed class CodeStatisticsCollector : AutoContext + { + private enum SessionState + { + Active, + Paused, + Finished + } + + private readonly CodeStatHub _hub; + private readonly CodeStatProcessor _session; + private SessionState _state = SessionState.Active; + + internal CodeStatisticsCollector(CodeStatHub hub, CodeStatProcessor session) + { + _hub = hub; + _session = session; + } + + [ContextMethod("Приостановить", "Pause")] + public void Pause() + { + EnsureState(SessionState.Active); + _hub.PauseSession(_session); + _state = SessionState.Paused; + } + + [ContextMethod("Восстановить", "Resume")] + public void Resume() + { + EnsureState(SessionState.Paused); + _hub.ResumeSession(_session); + _state = SessionState.Active; + } + + [ContextMethod("Завершить", "Finish")] + public ValueTable Finish() + { + if (_state == SessionState.Finished) + ThrowInvalidState(); + + _hub.FinishSession(_session); + _state = SessionState.Finished; + return ToValueTable(_session.GetStatData()); + } + + private void EnsureState(SessionState expected) + { + if (_state != expected) + ThrowInvalidState(); + } + + private static void ThrowInvalidState() + { + throw new RuntimeException( + "Неверное состояние сборщика статистики кода", + "Invalid code statistics collector state"); + } + + private static ValueTable ToValueTable(CodeStatDataCollection data) + { + var table = new ValueTable(); + var pathColumn = table.Columns.Add("Путь"); + var methodColumn = table.Columns.Add("Метод"); + var lineColumn = table.Columns.Add("НомерСтроки"); + var countColumn = table.Columns.Add("Количество"); + var timeColumn = table.Columns.Add("Время"); + + foreach (var item in data + .OrderBy(x => x.Entry.ScriptFileName) + .ThenBy(x => x.Entry.SubName) + .ThenBy(x => x.Entry.LineNumber)) + { + var row = table.Add(); + row.Set(pathColumn, ValueFactory.Create(item.Entry.ScriptFileName ?? string.Empty)); + row.Set(methodColumn, ValueFactory.Create(item.Entry.SubName ?? string.Empty)); + row.Set(lineColumn, ValueFactory.Create(item.Entry.LineNumber)); + row.Set(countColumn, ValueFactory.Create(item.ExecutionCount)); + row.Set(timeColumn, ValueFactory.Create((decimal)item.TimeElapsed)); + } + + return table; + } + } +} diff --git a/src/OneScript.StandardLibrary/CodeStatisticsContext.cs b/src/OneScript.StandardLibrary/CodeStatisticsContext.cs new file mode 100644 index 000000000..fb5f6c2ad --- /dev/null +++ b/src/OneScript.StandardLibrary/CodeStatisticsContext.cs @@ -0,0 +1,42 @@ +/*---------------------------------------------------------- +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 OneScript.Contexts; +using OneScript.Exceptions; +using OneScript.Execution; +using ScriptEngine.Machine; +using ScriptEngine.Machine.Contexts; + +namespace OneScript.StandardLibrary +{ + /// + /// Глобальный менеджер сбора статистики исполнения кода. + /// + [ContextClass("СборСтатистики", "CodeStatistics")] + public sealed class CodeStatisticsContext : AutoContext + { + [ContextMethod("СборДоступен", "CollectionAvailable")] + public bool CollectionAvailable(IBslProcess process) + { + return process.Services.TryResolve() != null; + } + + [ContextMethod("НачатьСбор", "StartCollection")] + public CodeStatisticsCollector StartCollection(IBslProcess process) + { + if (process.Services.TryResolve() is not CodeStatHub hub) + { + throw new RuntimeException( + "Сбор статистики кода не включён. Запустите приложение с параметром -codestat", + "Code statistics collection is not enabled. Start the application with the -codestat switch"); + } + + var session = hub.StartSession(); + return new CodeStatisticsCollector(hub, session); + } + } +} diff --git a/src/ScriptEngine.HostedScript/HostedScriptEngine.cs b/src/ScriptEngine.HostedScript/HostedScriptEngine.cs index 6ed02ed64..bc0b05319 100644 --- a/src/ScriptEngine.HostedScript/HostedScriptEngine.cs +++ b/src/ScriptEngine.HostedScript/HostedScriptEngine.cs @@ -53,6 +53,9 @@ private void SetGlobalContexts(IGlobalsManager manager) var bgTasksManager = new BackgroundTasksManager(_engine.Services.Resolve()); _env.InjectGlobalProperty(bgTasksManager, "ФоновыеЗадания", "BackgroundJobs", true); + + var codeStatistics = new CodeStatisticsContext(); + _env.InjectGlobalProperty(codeStatistics, "СборСтатистики", "CodeStatistics", true); } public void Initialize() diff --git a/src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs b/src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs new file mode 100644 index 000000000..482531393 --- /dev/null +++ b/src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs @@ -0,0 +1,148 @@ +/*---------------------------------------------------------- +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.Generic; + +namespace ScriptEngine.Machine +{ + public sealed class CodeStatHub : ICodeStatCollector + { + private readonly object _lock = new object(); + private readonly HashSet _knownEntries = new HashSet(); + private readonly HashSet _preparedScripts = new HashSet(); + + private CodeStatProcessor[] _alive = Array.Empty(); + private CodeStatProcessor[] _active = Array.Empty(); + + public CodeStatProcessor StartSession() + { + var session = new CodeStatProcessor(); + lock (_lock) + { + foreach (var entry in _knownEntries) + session.MarkEntryReached(entry, 0); + foreach (var script in _preparedScripts) + session.MarkPrepared(script); + + _alive = Append(_alive, session); + _active = Append(_active, session); + } + + return session; + } + + public void PauseSession(CodeStatProcessor session) + { + session.StopActiveWatch(); + lock (_lock) + { + _active = Remove(_active, session); + } + } + + public void ResumeSession(CodeStatProcessor session) + { + lock (_lock) + { + if (Array.IndexOf(_active, session) >= 0) + return; + _active = Append(_active, session); + } + } + + public void FinishSession(CodeStatProcessor session) + { + session.EndCodeStat(); + lock (_lock) + { + _active = Remove(_active, session); + _alive = Remove(_alive, session); + } + } + + public bool IsPrepared(string ScriptFileName) + { + lock (_lock) + { + return _preparedScripts.Contains(ScriptFileName); + } + } + + public void MarkEntryReached(CodeStatEntry entry, int count = 1) + { + CodeStatProcessor[] targets; + lock (_lock) + { + _knownEntries.Add(entry); + targets = count == 0 ? _alive : _active; + } + + foreach (var session in targets) + session.MarkEntryReached(entry, count); + } + + public void MarkPrepared(string scriptFileName) + { + CodeStatProcessor[] targets; + lock (_lock) + { + _preparedScripts.Add(scriptFileName); + targets = _alive; + } + + foreach (var session in targets) + session.MarkPrepared(scriptFileName); + } + + public void StopWatch(CodeStatEntry entry) + { + var targets = SnapshotActive(); + foreach (var session in targets) + session.StopWatch(entry); + } + + public void ResumeWatch(CodeStatEntry entry) + { + var targets = SnapshotActive(); + foreach (var session in targets) + session.ResumeWatch(entry); + } + + private CodeStatProcessor[] SnapshotActive() + { + lock (_lock) + { + return _active; + } + } + + private static T[] Append(T[] source, T item) + { + var result = new T[source.Length + 1]; + Array.Copy(source, result, source.Length); + result[source.Length] = item; + return result; + } + + private static T[] Remove(T[] source, T item) where T : class + { + var index = Array.IndexOf(source, item); + if (index < 0) + return source; + if (source.Length == 1) + return Array.Empty(); + + var result = new T[source.Length - 1]; + if (index > 0) + Array.Copy(source, 0, result, 0, index); + if (index < source.Length - 1) + Array.Copy(source, index + 1, result, index, source.Length - index - 1); + return result; + } + } +} diff --git a/src/ScriptEngine/Machine/CodeStat/CodeStatProcessor.cs b/src/ScriptEngine/Machine/CodeStat/CodeStatProcessor.cs index 15be6508f..6c8363ce3 100644 --- a/src/ScriptEngine/Machine/CodeStat/CodeStatProcessor.cs +++ b/src/ScriptEngine/Machine/CodeStat/CodeStatProcessor.cs @@ -38,7 +38,12 @@ public void MarkEntryReached(CodeStatEntry entry, int count = 1) else { _activeStopwatch?.Stop(); - _activeStopwatch = _watchers[entry]; + if (!_watchers.TryGetValue(entry, out var watch)) + { + watch = new Stopwatch(); + _watchers[entry] = watch; + } + _activeStopwatch = watch; _activeStopwatch.Start(); } } @@ -64,8 +69,14 @@ public CodeStatDataCollection GetStatData() } public void EndCodeStat() + { + StopActiveWatch(); + } + + public void StopActiveWatch() { _activeStopwatch?.Stop(); + _activeStopwatch = null; } public void StopWatch(CodeStatEntry entry) diff --git a/src/Tests/OneScript.Core.Tests/CodeStatHubTests.cs b/src/Tests/OneScript.Core.Tests/CodeStatHubTests.cs new file mode 100644 index 000000000..a30a13682 --- /dev/null +++ b/src/Tests/OneScript.Core.Tests/CodeStatHubTests.cs @@ -0,0 +1,124 @@ +/*---------------------------------------------------------- +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.Linq; +using FluentAssertions; +using ScriptEngine.Machine; +using Xunit; + +namespace OneScript.Core.Tests +{ + public class CodeStatHubTests + { + [Fact] + public void Hits_Go_To_Every_Active_Session() + { + var hub = new CodeStatHub(); + var first = hub.StartSession(); + var second = hub.StartSession(); + var entry = new CodeStatEntry("script.os", "Method", 1); + + hub.MarkEntryReached(entry, 0); + hub.MarkPrepared("script.os"); + hub.MarkEntryReached(entry); + + CountOf(first, entry).Should().Be(1); + CountOf(second, entry).Should().Be(1); + } + + [Fact] + public void New_Session_Gets_Prepared_Zeros_Without_History() + { + var hub = new CodeStatHub(); + var first = hub.StartSession(); + var entry = new CodeStatEntry("script.os", "Method", 1); + + hub.MarkEntryReached(entry, 0); + hub.MarkPrepared("script.os"); + hub.MarkEntryReached(entry); + hub.MarkEntryReached(entry); + + var second = hub.StartSession(); + + CountOf(first, entry).Should().Be(2); + CountOf(second, entry).Should().Be(0); + + hub.MarkEntryReached(entry); + + CountOf(first, entry).Should().Be(3); + CountOf(second, entry).Should().Be(1); + } + + [Fact] + public void Paused_Session_Does_Not_Receive_Hits() + { + var hub = new CodeStatHub(); + var session = hub.StartSession(); + var entry = new CodeStatEntry("script.os", "Method", 1); + + hub.MarkEntryReached(entry, 0); + hub.MarkPrepared("script.os"); + hub.MarkEntryReached(entry); + + hub.PauseSession(session); + hub.MarkEntryReached(entry); + hub.MarkEntryReached(entry); + + CountOf(session, entry).Should().Be(1); + + hub.ResumeSession(session); + hub.MarkEntryReached(entry); + + CountOf(session, entry).Should().Be(2); + } + + [Fact] + public void Paused_Session_Receives_Prepared_Zeros() + { + var hub = new CodeStatHub(); + var session = hub.StartSession(); + hub.PauseSession(session); + + var entry = new CodeStatEntry("other.os", "Other", 2); + hub.MarkEntryReached(entry, 0); + hub.MarkPrepared("other.os"); + + CountOf(session, entry).Should().Be(0); + session.GetStatData().Should().Contain(x => x.Entry.Equals(entry)); + } + + [Fact] + public void Finished_Session_Does_Not_Receive_Further_Events() + { + var hub = new CodeStatHub(); + var finished = hub.StartSession(); + var alive = hub.StartSession(); + var first = new CodeStatEntry("script.os", "Method", 1); + + hub.MarkEntryReached(first, 0); + hub.MarkPrepared("script.os"); + hub.MarkEntryReached(first); + hub.FinishSession(finished); + + hub.MarkEntryReached(first); + + var second = new CodeStatEntry("later.os", "Later", 3); + hub.MarkEntryReached(second, 0); + hub.MarkPrepared("later.os"); + + CountOf(finished, first).Should().Be(1); + finished.GetStatData().Should().NotContain(x => x.Entry.Equals(second)); + CountOf(alive, first).Should().Be(2); + CountOf(alive, second).Should().Be(0); + } + + private static int CountOf(CodeStatProcessor session, CodeStatEntry entry) + { + return session.GetStatData().Single(x => x.Entry.Equals(entry)).ExecutionCount; + } + } +} diff --git a/src/oscript/BehaviorSelector.cs b/src/oscript/BehaviorSelector.cs index 264cfb64f..db6337ab1 100644 --- a/src/oscript/BehaviorSelector.cs +++ b/src/oscript/BehaviorSelector.cs @@ -60,14 +60,14 @@ private static AppBehavior SelectParametrized(CmdLineHelper helper) private static AppBehavior EnableCodeStatistics(CmdLineHelper helper) { var param = helper.Parse(helper.Current()); - if (string.IsNullOrEmpty(param.Value)) - return null; - - var outputStatFile = param.Value; + var outputStatFile = string.IsNullOrEmpty(param.Value) ? null : param.Value; var behavior = Select(helper.Tail()); if (behavior is ExecuteScriptBehavior executor) + { + executor.CodeStatisticsEnabled = true; executor.CodeStatFile = outputStatFile; + } return behavior; } diff --git a/src/oscript/ExecuteScriptBehavior.cs b/src/oscript/ExecuteScriptBehavior.cs index 49cbc29c4..bca291ee6 100644 --- a/src/oscript/ExecuteScriptBehavior.cs +++ b/src/oscript/ExecuteScriptBehavior.cs @@ -29,7 +29,7 @@ public ExecuteScriptBehavior(string path, string[] args) public string CodeStatFile { get; set; } - public bool CodeStatisticsEnabled => CodeStatFile != null; + public bool CodeStatisticsEnabled { get; set; } public override int Execute() { @@ -43,11 +43,15 @@ public override int Execute() var builder = ConsoleHostBuilder.Create(_path); builder.WithDebugger(DebugController); - CodeStatProcessor codeStatProcessor = null; + CodeStatHub codeStatHub = null; + CodeStatProcessor cliSession = null; if (CodeStatisticsEnabled) { - codeStatProcessor = new CodeStatProcessor(); - builder.Services.RegisterSingleton(codeStatProcessor); + codeStatHub = new CodeStatHub(); + builder.Services.RegisterSingleton(codeStatHub); + builder.Services.RegisterSingleton(codeStatHub); + if (CodeStatFile != null) + cliSession = codeStatHub.StartSession(); } var hostedScript = ConsoleHostBuilder.Build(builder); @@ -67,12 +71,11 @@ public override int Execute() var result = process.Start(); hostedScript.Dispose(); - if (codeStatProcessor != null) + if (cliSession != null && codeStatHub != null) { - codeStatProcessor.EndCodeStat(); - var codeStat = codeStatProcessor.GetStatData(); + codeStatHub.FinishSession(cliSession); var statsWriter = new CodeStatWriter(CodeStatFile, CodeStatWriterType.JSON); - statsWriter.Write(codeStat); + statsWriter.Write(cliSession.GetStatData()); } return result; diff --git a/src/oscript/ShowUsageBehavior.cs b/src/oscript/ShowUsageBehavior.cs index ee1c26a57..ec652dabd 100644 --- a/src/oscript/ShowUsageBehavior.cs +++ b/src/oscript/ShowUsageBehavior.cs @@ -39,7 +39,7 @@ public override int Execute() Output.WriteLine("Options:"); Output.WriteLine($" {"-encoding=",modeWidth} Set output encoding (e.g. utf-8)."); - Output.WriteLine($" {"-codestat=",modeWidth} Write code execution statistics to file."); + Output.WriteLine($" {"-codestat[=]",modeWidth} Enable code statistics; write to file if specified."); Output.WriteLine(); Output.WriteLine("CGI Mode:"); diff --git a/tests/codestat-api.os b/tests/codestat-api.os new file mode 100644 index 000000000..932f0a52c --- /dev/null +++ b/tests/codestat-api.os @@ -0,0 +1,312 @@ +/////////////////////////////////////////////////////////////////////// +// +// Сбор статистики из исполняемого кода +// +/////////////////////////////////////////////////////////////////////// + +Перем юТест; + +Функция ПолучитьСписокТестов(ЮнитТестирование) Экспорт + + юТест = ЮнитТестирование; + + ВсеТесты = Новый Массив; + ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоСборСтатистикиЕстьВГлобальномКонтексте"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоСборНедоступенБезФлага"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоНачатьСборБросаетБезФлага"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьСборДоступенИРезультатЗавершения"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьПересекающиесяСборы"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьПаузуИВосстановление"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьПереходыСостояний"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьДампВФайл"); + + Возврат ВсеТесты; + +КонецФункции + +Процедура ТестДолжен_ПроверитьЧтоСборСтатистикиЕстьВГлобальномКонтексте() Экспорт + + юТест.ПроверитьРавенство(Тип("СборСтатистики"), ТипЗнч(СборСтатистики)); + юТест.ПроверитьЛожь(СборСтатистики.СборДоступен()); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьЧтоСборНедоступенБезФлага() Экспорт + + юТест.ПроверитьЛожь(СборСтатистики.СборДоступен(), "Без -codestat сбор должен быть недоступен"); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьЧтоНачатьСборБросаетБезФлага() Экспорт + + Бросило = Ложь; + Попытка + СборСтатистики.НачатьСбор(); + Исключение + Бросило = Истина; + юТест.ПроверитьИстину( + СтрНайти(ИнформацияОбОшибке().Описание, "-codestat") > 0, + "Текст исключения должен упоминать -codestat" + ); + КонецПопытки; + + юТест.ПроверитьИстину(Бросило, "НачатьСбор без -codestat должен выбрасывать исключение"); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьСборДоступенИРезультатЗавершения() Экспорт + + ТекстСкрипта = + "Процедура ВыполняемыйМетод() + | Значение = 1; + |КонецПроцедуры + | + |Функция КоличествоХитов(Таблица, ИмяМетода) + | Сумма = 0; + | Для Каждого СтрокаТаблицы Из Таблица Цикл + | Если СтрокаТаблицы.Метод = ИмяМетода Тогда + | Сумма = Сумма + СтрокаТаблицы.Количество; + | КонецЕсли; + | КонецЦикла; + | Возврат Сумма; + |КонецФункции + | + |Если Не СборСтатистики.СборДоступен() Тогда + | ВызватьИсключение ""Сбор должен быть доступен""; + |КонецЕсли; + | + |Сбор = СборСтатистики.НачатьСбор(); + |ВыполняемыйМетод(); + |Таблица = Сбор.Завершить(); + | + |Если ТипЗнч(Таблица) <> Тип(""ТаблицаЗначений"") Тогда + | ВызватьИсключение ""Завершить должен вернуть ТаблицаЗначений""; + |КонецЕсли; + | + |Для Каждого ИмяКолонки Из СтрРазделить(""Путь,Метод,НомерСтроки,Количество,Время"", "","") Цикл + | Если Таблица.Колонки.Найти(ИмяКолонки) = Неопределено Тогда + | ВызватьИсключение ""Нет колонки "" + ИмяКолонки; + | КонецЕсли; + |КонецЦикла; + | + |Если КоличествоХитов(Таблица, ""ВыполняемыйМетод"") = 0 Тогда + | ВызватьИсключение ""В результате должны быть хиты ВыполняемыйМетод""; + |КонецЕсли;"; + + ПроверитьУспешныйЗапуск(ТекстСкрипта); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьПересекающиесяСборы() Экспорт + + ТекстСкрипта = + "Процедура ТолькоА() + | Значение = 1; + |КонецПроцедуры + | + |Процедура ОбщийКод() + | Значение = 2; + |КонецПроцедуры + | + |Функция КоличествоХитов(Таблица, ИмяМетода) + | Сумма = 0; + | Для Каждого СтрокаТаблицы Из Таблица Цикл + | Если СтрокаТаблицы.Метод = ИмяМетода Тогда + | Сумма = Сумма + СтрокаТаблицы.Количество; + | КонецЕсли; + | КонецЦикла; + | Возврат Сумма; + |КонецФункции + | + |СборА = СборСтатистики.НачатьСбор(); + |ТолькоА(); + | + |СборБ = СборСтатистики.НачатьСбор(); + |ОбщийКод(); + | + |ТаблицаБ = СборБ.Завершить(); + |ТолькоА(); + |ТаблицаА = СборА.Завершить(); + | + |Если КоличествоХитов(ТаблицаА, ""ТолькоА"") = 0 Тогда + | ВызватьИсключение ""Сбор А должен содержать ТолькоА""; + |КонецЕсли; + |Если КоличествоХитов(ТаблицаБ, ""ТолькоА"") <> 0 Тогда + | ВызватьИсключение ""Сбор Б не должен содержать хиты ТолькоА""; + |КонецЕсли; + |Если КоличествоХитов(ТаблицаА, ""ОбщийКод"") = 0 Тогда + | ВызватьИсключение ""Сбор А должен содержать ОбщийКод""; + |КонецЕсли; + |Если КоличествоХитов(ТаблицаБ, ""ОбщийКод"") = 0 Тогда + | ВызватьИсключение ""Сбор Б должен содержать ОбщийКод""; + |КонецЕсли;"; + + ПроверитьУспешныйЗапуск(ТекстСкрипта); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьПаузуИВосстановление() Экспорт + + ТекстСкрипта = + "Процедура ДоПаузы() + | Значение = 1; + |КонецПроцедуры + | + |Процедура НаПаузе() + | Значение = 2; + |КонецПроцедуры + | + |Процедура ПослеПаузы() + | Значение = 3; + |КонецПроцедуры + | + |Функция КоличествоХитов(Таблица, ИмяМетода) + | Сумма = 0; + | Для Каждого СтрокаТаблицы Из Таблица Цикл + | Если СтрокаТаблицы.Метод = ИмяМетода Тогда + | Сумма = Сумма + СтрокаТаблицы.Количество; + | КонецЕсли; + | КонецЦикла; + | Возврат Сумма; + |КонецФункции + | + |Сбор = СборСтатистики.НачатьСбор(); + |ДоПаузы(); + |Сбор.Приостановить(); + |НаПаузе(); + |Сбор.Восстановить(); + |ПослеПаузы(); + |Таблица = Сбор.Завершить(); + | + |Если КоличествоХитов(Таблица, ""ДоПаузы"") = 0 Тогда + | ВызватьИсключение ""Должны быть хиты ДоПаузы""; + |КонецЕсли; + |Если КоличествоХитов(Таблица, ""НаПаузе"") <> 0 Тогда + | ВызватьИсключение ""На паузе хиты не должны копиться""; + |КонецЕсли; + |Если КоличествоХитов(Таблица, ""ПослеПаузы"") = 0 Тогда + | ВызватьИсключение ""Должны быть хиты ПослеПаузы""; + |КонецЕсли;"; + + ПроверитьУспешныйЗапуск(ТекстСкрипта); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьПереходыСостояний() Экспорт + + ТекстСкрипта = + "Процедура ОжидатьИсключение(Сборщик, ИмяМетода, Описание) + | Бросило = Ложь; + | Попытка + | Если ИмяМетода = ""Восстановить"" Тогда + | Сборщик.Восстановить(); + | ИначеЕсли ИмяМетода = ""Приостановить"" Тогда + | Сборщик.Приостановить(); + | ИначеЕсли ИмяМетода = ""Завершить"" Тогда + | Сборщик.Завершить(); + | КонецЕсли; + | Исключение + | Бросило = Истина; + | Если СтрНайти(ИнформацияОбОшибке().Описание, ""Неверное состояние"") = 0 Тогда + | ВызватьИсключение; + | КонецЕсли; + | КонецПопытки; + | Если Не Бросило Тогда + | ВызватьИсключение Описание + "": ожидалось исключение""; + | КонецЕсли; + |КонецПроцедуры + | + |Сбор = СборСтатистики.НачатьСбор(); + |ОжидатьИсключение(Сбор, ""Восстановить"", ""Восстановить из Active""); + | + |Сбор.Приостановить(); + |ОжидатьИсключение(Сбор, ""Приостановить"", ""Повторная пауза""); + | + |Сбор.Восстановить(); + |Таблица = Сбор.Завершить(); + |Если ТипЗнч(Таблица) <> Тип(""ТаблицаЗначений"") Тогда + | ВызватьИсключение ""Завершить из Active должен вернуть таблицу""; + |КонецЕсли; + | + |ОжидатьИсключение(Сбор, ""Завершить"", ""Повторный Завершить""); + |ОжидатьИсключение(Сбор, ""Приостановить"", ""Пауза после Завершить""); + |ОжидатьИсключение(Сбор, ""Восстановить"", ""Восстановить после Завершить""); + | + |СборСПаузы = СборСтатистики.НачатьСбор(); + |СборСПаузы.Приостановить(); + |ТаблицаСПаузы = СборСПаузы.Завершить(); + |Если ТипЗнч(ТаблицаСПаузы) <> Тип(""ТаблицаЗначений"") Тогда + | ВызватьИсключение ""Завершить из Paused должен вернуть таблицу""; + |КонецЕсли;"; + + ПроверитьУспешныйЗапуск(ТекстСкрипта); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьДампВФайл() Экспорт + + ИмяФайлаСкрипта = ПолучитьИмяВременногоФайла("os"); + ИмяФайлаСтатистики = ПолучитьИмяВременногоФайла("json"); + + Запись = Новый ЗаписьТекста(ИмяФайлаСкрипта, КодировкаТекста.UTF8); + Запись.Записать("Значение = 1;"); + Запись.Закрыть(); + + Попытка + Результат = ЗапуститьОскрипт("-codestat=" + ИмяФайлаСтатистики + " """ + ИмяФайлаСкрипта + """"); + юТест.ПроверитьРавенство(0, Результат.КодВозврата, Результат.Вывод); + + ФайлСтатистики = Новый Файл(ИмяФайлаСтатистики); + юТест.ПроверитьИстину(ФайлСтатистики.Существует(), "Файл статистики должен быть записан"); + юТест.ПроверитьБольше(ФайлСтатистики.Размер(), 0, "Файл статистики не должен быть пустым"); + Исключение + УдалитьФайлы(ИмяФайлаСкрипта); + УдалитьФайлы(ИмяФайлаСтатистики); + ВызватьИсключение; + КонецПопытки; + + УдалитьФайлы(ИмяФайлаСкрипта); + УдалитьФайлы(ИмяФайлаСтатистики); + +КонецПроцедуры + +Процедура ПроверитьУспешныйЗапуск(Знач ТекстСкрипта) + + ИмяФайлаСкрипта = ПолучитьИмяВременногоФайла("os"); + Запись = Новый ЗаписьТекста(ИмяФайлаСкрипта, КодировкаТекста.UTF8); + Запись.Записать(ТекстСкрипта); + Запись.Закрыть(); + + Попытка + Результат = ЗапуститьОскрипт("-codestat """ + ИмяФайлаСкрипта + """"); + юТест.ПроверитьРавенство(0, Результат.КодВозврата, Результат.Вывод); + Исключение + УдалитьФайлы(ИмяФайлаСкрипта); + ВызватьИсключение; + КонецПопытки; + + УдалитьФайлы(ИмяФайлаСкрипта); + +КонецПроцедуры + +Функция ЗапуститьОскрипт(Знач Аргументы) + + СтрокаЗапуска = ПутьОСкрипт() + " " + Аргументы; + Процесс = СоздатьПроцесс(СтрокаЗапуска, , Истина); + Процесс.Запустить(); + + Вывод = ""; + Пока Не Процесс.Завершен Или Процесс.ПотокВывода.ЕстьДанные Цикл + Стр = Процесс.ПотокВывода.Прочитать(); + Если Стр <> Неопределено Тогда + Вывод = Вывод + Стр; + КонецЕсли; + КонецЦикла; + + Возврат Новый Структура("КодВозврата, Вывод", Процесс.КодВозврата, Вывод); + +КонецФункции + +Функция ПутьОСкрипт() + Возврат "dotnet """ + ОбъединитьПути(КаталогПрограммы(), "oscript.dll") + """"; +КонецФункции diff --git a/tests/process.os b/tests/process.os index 44da651b5..44a385b04 100644 --- a/tests/process.os +++ b/tests/process.os @@ -162,7 +162,7 @@ | |Options: | -encoding= Set output encoding (e.g. utf-8). - | -codestat= Write code execution statistics to file. + | -codestat[=] Enable code statistics; write to file if specified. | |CGI Mode: | oscript.exe -cgi [script_arguments...] From 718210b1d0a988e9855da50cdd9bb4780c963f30 Mon Sep 17 00:00:00 2001 From: Sergey Batanov Date: Wed, 2 Sep 2026 22:11:00 +0300 Subject: [PATCH 2/6] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D0=BD=D0=B0=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B1=D0=BB=D0=BE=D0=BA=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=BE=D0=BA.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Machine/CodeStat/CodeStatHub.cs | 12 ++--- .../OneScript.Core.Tests/CodeStatHubTests.cs | 50 +++++++++++++++++++ 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs b/src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs index 482531393..d216b03ee 100644 --- a/src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs +++ b/src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs @@ -38,9 +38,9 @@ public CodeStatProcessor StartSession() public void PauseSession(CodeStatProcessor session) { - session.StopActiveWatch(); lock (_lock) { + session.StopActiveWatch(); _active = Remove(_active, session); } } @@ -57,9 +57,9 @@ public void ResumeSession(CodeStatProcessor session) public void FinishSession(CodeStatProcessor session) { - session.EndCodeStat(); lock (_lock) { + session.EndCodeStat(); _active = Remove(_active, session); _alive = Remove(_alive, session); } @@ -75,15 +75,13 @@ public bool IsPrepared(string ScriptFileName) public void MarkEntryReached(CodeStatEntry entry, int count = 1) { - CodeStatProcessor[] targets; lock (_lock) { _knownEntries.Add(entry); - targets = count == 0 ? _alive : _active; + var targets = count == 0 ? _alive : _active; + foreach (var session in targets) + session.MarkEntryReached(entry, count); } - - foreach (var session in targets) - session.MarkEntryReached(entry, count); } public void MarkPrepared(string scriptFileName) diff --git a/src/Tests/OneScript.Core.Tests/CodeStatHubTests.cs b/src/Tests/OneScript.Core.Tests/CodeStatHubTests.cs index a30a13682..db68be64a 100644 --- a/src/Tests/OneScript.Core.Tests/CodeStatHubTests.cs +++ b/src/Tests/OneScript.Core.Tests/CodeStatHubTests.cs @@ -5,7 +5,10 @@ This Source Code Form is subject to the terms of the at http://mozilla.org/MPL/2.0/. ----------------------------------------------------------*/ +using System; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using FluentAssertions; using ScriptEngine.Machine; using Xunit; @@ -116,9 +119,56 @@ public void Finished_Session_Does_Not_Receive_Further_Events() CountOf(alive, second).Should().Be(0); } + [Fact] + public void Concurrent_Hits_Do_Not_Apply_After_Pause_Or_Finish() + { + AssertHitsDoNotApplyAfterControl(pause: true); + AssertHitsDoNotApplyAfterControl(pause: false); + } + + private static void AssertHitsDoNotApplyAfterControl(bool pause) + { + var hub = new CodeStatHub(); + var session = hub.StartSession(); + var entry = new CodeStatEntry("script.os", "Method", 1); + hub.MarkEntryReached(entry, 0); + hub.MarkPrepared("script.os"); + + using var stopHitting = new ManualResetEventSlim(false); + var start = new Barrier(2); + + var hitter = Task.Run(() => + { + start.SignalAndWait(); + while (!stopHitting.IsSet) + hub.MarkEntryReached(entry); + }); + + start.SignalAndWait(); + if (pause) + hub.PauseSession(session); + else + hub.FinishSession(session); + + var countWhenControlReturned = CountOf(session, entry); + var timeWhenControlReturned = TimeOf(session, entry); + + Thread.Sleep(30); + stopHitting.Set(); + hitter.Wait(); + + CountOf(session, entry).Should().Be(countWhenControlReturned); + TimeOf(session, entry).Should().Be(timeWhenControlReturned); + } + private static int CountOf(CodeStatProcessor session, CodeStatEntry entry) { return session.GetStatData().Single(x => x.Entry.Equals(entry)).ExecutionCount; } + + private static long TimeOf(CodeStatProcessor session, CodeStatEntry entry) + { + return session.GetStatData().Single(x => x.Entry.Equals(entry)).TimeElapsed; + } } } From c3923875a4e615f6f094ce52c4caafd016a654be Mon Sep 17 00:00:00 2001 From: Sergey Batanov Date: Thu, 3 Sep 2026 21:38:36 +0300 Subject: [PATCH 3/6] =?UTF-8?q?=D0=9E=D0=BF=D1=82=D0=B8=D0=BC=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D1=81=D1=82=D0=B0=D1=80=D1=82=D0=B0?= =?UTF-8?q?=20=D1=81=D0=B1=D0=BE=D1=80=D0=B0=20=D1=81=D1=82=D0=B0=D1=82?= =?UTF-8?q?=D0=B8=D1=81=D1=82=D0=B8=D0=BA=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Machine/CodeStat/CodeStatHub.cs | 34 +++--- .../Machine/CodeStat/CodeStatProcessor.cs | 115 ++++++++++++------ .../OneScript.Core.Tests/CodeStatHubTests.cs | 17 +++ tests/codestat-api.os | 55 ++++++++- 4 files changed, 166 insertions(+), 55 deletions(-) diff --git a/src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs b/src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs index d216b03ee..c9b493019 100644 --- a/src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs +++ b/src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs @@ -13,7 +13,8 @@ namespace ScriptEngine.Machine public sealed class CodeStatHub : ICodeStatCollector { private readonly object _lock = new object(); - private readonly HashSet _knownEntries = new HashSet(); + private readonly List _knownEntries = new List(); + private readonly HashSet _knownSet = new HashSet(); private readonly HashSet _preparedScripts = new HashSet(); private CodeStatProcessor[] _alive = Array.Empty(); @@ -21,14 +22,9 @@ public sealed class CodeStatHub : ICodeStatCollector public CodeStatProcessor StartSession() { - var session = new CodeStatProcessor(); + var session = new CodeStatProcessor(this); lock (_lock) { - foreach (var entry in _knownEntries) - session.MarkEntryReached(entry, 0); - foreach (var script in _preparedScripts) - session.MarkPrepared(script); - _alive = Append(_alive, session); _active = Append(_active, session); } @@ -60,11 +56,20 @@ public void FinishSession(CodeStatProcessor session) lock (_lock) { session.EndCodeStat(); + session.FreezeCatalog(_knownEntries.ToArray(), new HashSet(_preparedScripts)); _active = Remove(_active, session); _alive = Remove(_alive, session); } } + internal CodeStatDataCollection GetLiveStatData(CodeStatProcessor session) + { + lock (_lock) + { + return session.BuildFromCatalog(_knownEntries, _knownEntries.Count, _preparedScripts); + } + } + public bool IsPrepared(string ScriptFileName) { lock (_lock) @@ -77,24 +82,23 @@ public void MarkEntryReached(CodeStatEntry entry, int count = 1) { lock (_lock) { - _knownEntries.Add(entry); - var targets = count == 0 ? _alive : _active; - foreach (var session in targets) + if (_knownSet.Add(entry)) + _knownEntries.Add(entry); + + if (count == 0) + return; + + foreach (var session in _active) session.MarkEntryReached(entry, count); } } public void MarkPrepared(string scriptFileName) { - CodeStatProcessor[] targets; lock (_lock) { _preparedScripts.Add(scriptFileName); - targets = _alive; } - - foreach (var session in targets) - session.MarkPrepared(scriptFileName); } public void StopWatch(CodeStatEntry entry) diff --git a/src/ScriptEngine/Machine/CodeStat/CodeStatProcessor.cs b/src/ScriptEngine/Machine/CodeStat/CodeStatProcessor.cs index 6c8363ce3..028ae38e4 100644 --- a/src/ScriptEngine/Machine/CodeStat/CodeStatProcessor.cs +++ b/src/ScriptEngine/Machine/CodeStat/CodeStatProcessor.cs @@ -1,21 +1,35 @@ /*---------------------------------------------------------- -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 +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.Diagnostics; using System.Collections.Generic; +using System.Diagnostics; 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 CodeStatHub _hub; + private readonly Dictionary _codeStat = new Dictionary(); + private readonly Dictionary _watchers = new Dictionary(); + private readonly HashSet _preparedScripts = new HashSet(); + + private Stopwatch _activeStopwatch; + private IReadOnlyList _frozenEntries; + private int _frozenCount; + private HashSet _frozenPrepared; + + public CodeStatProcessor() + { + } + + internal CodeStatProcessor(CodeStatHub hub) + { + _hub = hub; + } public bool IsPrepared(string ScriptFileName) { @@ -24,28 +38,21 @@ public bool IsPrepared(string ScriptFileName) public void MarkEntryReached(CodeStatEntry entry, int count = 1) { - int oldValue = 0; - _codeStat.TryGetValue(entry, out oldValue); + _codeStat.TryGetValue(entry, out var oldValue); _codeStat[entry] = oldValue + count; if (count == 0) + return; + + _activeStopwatch?.Stop(); + if (!_watchers.TryGetValue(entry, out var watch)) { - if (!_watchers.ContainsKey(entry)) - { - _watchers.Add(entry, new Stopwatch()); - } - } - else - { - _activeStopwatch?.Stop(); - if (!_watchers.TryGetValue(entry, out var watch)) - { - watch = new Stopwatch(); - _watchers[entry] = watch; - } - _activeStopwatch = watch; - _activeStopwatch.Start(); + watch = new Stopwatch(); + _watchers[entry] = watch; } + + _activeStopwatch = watch; + _activeStopwatch.Start(); } public void MarkPrepared(string scriptFileName) @@ -55,16 +62,54 @@ public void MarkPrepared(string scriptFileName) public CodeStatDataCollection GetStatData() { - CodeStatDataCollection data = new CodeStatDataCollection(); + if (_frozenEntries != null) + return BuildFromCatalog(_frozenEntries, _frozenCount, _frozenPrepared); + + if (_hub != null) + return _hub.GetLiveStatData(this); + + var data = new CodeStatDataCollection(); foreach (var item in _codeStat) { if (!IsPrepared(item.Key.ScriptFileName)) - { continue; - } - data.Add(new CodeStatData(item.Key, _watchers[item.Key].ElapsedMilliseconds, item.Value)); + + long time = 0; + if (_watchers.TryGetValue(item.Key, out var watch)) + time = watch.ElapsedMilliseconds; + data.Add(new CodeStatData(item.Key, time, item.Value)); + } + + return data; + } + + internal void FreezeCatalog(CodeStatEntry[] entries, HashSet prepared) + { + _frozenEntries = entries; + _frozenCount = entries.Length; + _frozenPrepared = prepared; + } + + internal CodeStatDataCollection BuildFromCatalog( + IReadOnlyList entries, + int count, + HashSet prepared) + { + var data = new CodeStatDataCollection(); + for (var i = 0; i < count; i++) + { + var entry = entries[i]; + if (!prepared.Contains(entry.ScriptFileName)) + continue; + + _codeStat.TryGetValue(entry, out var executionCount); + long time = 0; + if (_watchers.TryGetValue(entry, out var watch)) + time = watch.ElapsedMilliseconds; + + data.Add(new CodeStatData(entry, time, executionCount)); } - + return data; } @@ -81,19 +126,17 @@ public void StopActiveWatch() public void StopWatch(CodeStatEntry entry) { - if (_watchers.ContainsKey(entry)) - { - _watchers[entry].Stop(); - } + if (_watchers.TryGetValue(entry, out var watch)) + watch.Stop(); } public void ResumeWatch(CodeStatEntry entry) { _activeStopwatch?.Stop(); - if (_watchers.ContainsKey(entry)) + if (_watchers.TryGetValue(entry, out var watch)) { - _activeStopwatch = _watchers[entry]; + _activeStopwatch = watch; _activeStopwatch.Start(); } } diff --git a/src/Tests/OneScript.Core.Tests/CodeStatHubTests.cs b/src/Tests/OneScript.Core.Tests/CodeStatHubTests.cs index db68be64a..d16a164e4 100644 --- a/src/Tests/OneScript.Core.Tests/CodeStatHubTests.cs +++ b/src/Tests/OneScript.Core.Tests/CodeStatHubTests.cs @@ -119,6 +119,23 @@ public void Finished_Session_Does_Not_Receive_Further_Events() CountOf(alive, second).Should().Be(0); } + [Fact] + public void StartSession_Reads_Zeros_From_Shared_Catalog() + { + var hub = new CodeStatHub(); + for (var line = 1; line <= 50; line++) + { + hub.MarkEntryReached(new CodeStatEntry("script.os", "Method", line), 0); + } + + hub.MarkPrepared("script.os"); + var session = hub.StartSession(); + var data = session.GetStatData(); + + data.Count.Should().Be(50); + data.Should().OnlyContain(x => x.ExecutionCount == 0 && x.TimeElapsed == 0); + } + [Fact] public void Concurrent_Hits_Do_Not_Apply_After_Pause_Or_Finish() { diff --git a/tests/codestat-api.os b/tests/codestat-api.os index 932f0a52c..a779f736b 100644 --- a/tests/codestat-api.os +++ b/tests/codestat-api.os @@ -61,6 +61,10 @@ | Значение = 1; |КонецПроцедуры | + |Процедура НевызываемыйМетод() + | Значение = 2; + |КонецПроцедуры + | |Функция КоличествоХитов(Таблица, ИмяМетода) | Сумма = 0; | Для Каждого СтрокаТаблицы Из Таблица Цикл @@ -71,6 +75,19 @@ | Возврат Сумма; |КонецФункции | + |Функция ВсеСтрокиМетодаСНулями(Таблица, ИмяМетода) + | Нашли = Ложь; + | Для Каждого СтрокаТаблицы Из Таблица Цикл + | Если СтрокаТаблицы.Метод = ИмяМетода Тогда + | Нашли = Истина; + | Если СтрокаТаблицы.Количество <> 0 Тогда + | Возврат Ложь; + | КонецЕсли; + | КонецЕсли; + | КонецЦикла; + | Возврат Нашли; + |КонецФункции + | |Если Не СборСтатистики.СборДоступен() Тогда | ВызватьИсключение ""Сбор должен быть доступен""; |КонецЕсли; @@ -91,6 +108,10 @@ | |Если КоличествоХитов(Таблица, ""ВыполняемыйМетод"") = 0 Тогда | ВызватьИсключение ""В результате должны быть хиты ВыполняемыйМетод""; + |КонецЕсли; + | + |Если Не ВсеСтрокиМетодаСНулями(Таблица, ""НевызываемыйМетод"") Тогда + | ВызватьИсключение ""Завершить должен вернуть непройденные строки НевызываемыйМетод с Количество = 0""; |КонецЕсли;"; ПроверитьУспешныйЗапуск(ТекстСкрипта); @@ -118,6 +139,19 @@ | Возврат Сумма; |КонецФункции | + |Функция ВсеСтрокиМетодаСНулями(Таблица, ИмяМетода) + | Нашли = Ложь; + | Для Каждого СтрокаТаблицы Из Таблица Цикл + | Если СтрокаТаблицы.Метод = ИмяМетода Тогда + | Нашли = Истина; + | Если СтрокаТаблицы.Количество <> 0 Тогда + | Возврат Ложь; + | КонецЕсли; + | КонецЕсли; + | КонецЦикла; + | Возврат Нашли; + |КонецФункции + | |СборА = СборСтатистики.НачатьСбор(); |ТолькоА(); | @@ -131,8 +165,8 @@ |Если КоличествоХитов(ТаблицаА, ""ТолькоА"") = 0 Тогда | ВызватьИсключение ""Сбор А должен содержать ТолькоА""; |КонецЕсли; - |Если КоличествоХитов(ТаблицаБ, ""ТолькоА"") <> 0 Тогда - | ВызватьИсключение ""Сбор Б не должен содержать хиты ТолькоА""; + |Если Не ВсеСтрокиМетодаСНулями(ТаблицаБ, ""ТолькоА"") Тогда + | ВызватьИсключение ""Сбор Б должен содержать непройденные строки ТолькоА с Количество = 0""; |КонецЕсли; |Если КоличествоХитов(ТаблицаА, ""ОбщийКод"") = 0 Тогда | ВызватьИсключение ""Сбор А должен содержать ОбщийКод""; @@ -170,6 +204,19 @@ | Возврат Сумма; |КонецФункции | + |Функция ВсеСтрокиМетодаСНулями(Таблица, ИмяМетода) + | Нашли = Ложь; + | Для Каждого СтрокаТаблицы Из Таблица Цикл + | Если СтрокаТаблицы.Метод = ИмяМетода Тогда + | Нашли = Истина; + | Если СтрокаТаблицы.Количество <> 0 Тогда + | Возврат Ложь; + | КонецЕсли; + | КонецЕсли; + | КонецЦикла; + | Возврат Нашли; + |КонецФункции + | |Сбор = СборСтатистики.НачатьСбор(); |ДоПаузы(); |Сбор.Приостановить(); @@ -181,8 +228,8 @@ |Если КоличествоХитов(Таблица, ""ДоПаузы"") = 0 Тогда | ВызватьИсключение ""Должны быть хиты ДоПаузы""; |КонецЕсли; - |Если КоличествоХитов(Таблица, ""НаПаузе"") <> 0 Тогда - | ВызватьИсключение ""На паузе хиты не должны копиться""; + |Если Не ВсеСтрокиМетодаСНулями(Таблица, ""НаПаузе"") Тогда + | ВызватьИсключение ""Завершить должен вернуть строки НаПаузе с Количество = 0""; |КонецЕсли; |Если КоличествоХитов(Таблица, ""ПослеПаузы"") = 0 Тогда | ВызватьИсключение ""Должны быть хиты ПослеПаузы""; From e8a424f17553126bda2db49595e987760f6a66a1 Mon Sep 17 00:00:00 2001 From: Sergey Batanov Date: Thu, 3 Sep 2026 21:59:01 +0300 Subject: [PATCH 4/6] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=20=D1=83=D1=81=D0=BB=D0=BE=D0=B2=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BF=D1=80=D0=B5=D0=B4=D0=BE=D1=82=D0=B2?= =?UTF-8?q?=D1=80=D0=B0=D1=89=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B2=D0=BE=D0=B7?= =?UTF-8?q?=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=B2=D0=B5=D1=80=D1=88=D0=B5=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D1=85=20=D1=81=D0=B5=D1=81=D1=81=D0=B8=D0=B9=20=D0=B8=20=D1=83?= =?UTF-8?q?=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=B0=20=D0=BF=D0=BE=D1=82?= =?UTF-8?q?=D0=BE=D0=BA=D0=BE=D0=B1=D0=B5=D0=B7=D0=BE=D0=BF=D0=B0=D1=81?= =?UTF-8?q?=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4?= =?UTF-8?q?=D0=B0=20ResumeWatch.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=BA=D0=BE=D1=80=D1=80=D0=B5=D0=BA=D1=82=D0=BD=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B8=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D1=8D?= =?UTF-8?q?=D1=82=D0=B8=D1=85=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B9.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Machine/CodeStat/CodeStatHub.cs | 20 +++---- .../OneScript.Core.Tests/CodeStatHubTests.cs | 59 +++++++++++++++++++ 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs b/src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs index c9b493019..3be620393 100644 --- a/src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs +++ b/src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs @@ -45,6 +45,8 @@ public void ResumeSession(CodeStatProcessor session) { lock (_lock) { + if (Array.IndexOf(_alive, session) < 0) + return; if (Array.IndexOf(_active, session) >= 0) return; _active = Append(_active, session); @@ -103,23 +105,19 @@ public void MarkPrepared(string scriptFileName) public void StopWatch(CodeStatEntry entry) { - var targets = SnapshotActive(); - foreach (var session in targets) - session.StopWatch(entry); + lock (_lock) + { + foreach (var session in _active) + session.StopWatch(entry); + } } public void ResumeWatch(CodeStatEntry entry) - { - var targets = SnapshotActive(); - foreach (var session in targets) - session.ResumeWatch(entry); - } - - private CodeStatProcessor[] SnapshotActive() { lock (_lock) { - return _active; + foreach (var session in _active) + session.ResumeWatch(entry); } } diff --git a/src/Tests/OneScript.Core.Tests/CodeStatHubTests.cs b/src/Tests/OneScript.Core.Tests/CodeStatHubTests.cs index d16a164e4..bbda928f5 100644 --- a/src/Tests/OneScript.Core.Tests/CodeStatHubTests.cs +++ b/src/Tests/OneScript.Core.Tests/CodeStatHubTests.cs @@ -136,6 +136,32 @@ public void StartSession_Reads_Zeros_From_Shared_Catalog() data.Should().OnlyContain(x => x.ExecutionCount == 0 && x.TimeElapsed == 0); } + [Fact] + public void ResumeSession_Does_Not_Revive_Finished_Session() + { + var hub = new CodeStatHub(); + var session = hub.StartSession(); + var entry = new CodeStatEntry("script.os", "Method", 1); + + hub.MarkEntryReached(entry, 0); + hub.MarkPrepared("script.os"); + hub.MarkEntryReached(entry); + hub.FinishSession(session); + + hub.ResumeSession(session); + hub.MarkEntryReached(entry); + hub.MarkEntryReached(entry); + + CountOf(session, entry).Should().Be(1); + } + + [Fact] + public void Concurrent_ResumeWatch_Does_Not_Restart_After_Pause_Or_Finish() + { + AssertWatchDoesNotResumeAfterControl(pause: true); + AssertWatchDoesNotResumeAfterControl(pause: false); + } + [Fact] public void Concurrent_Hits_Do_Not_Apply_After_Pause_Or_Finish() { @@ -143,6 +169,39 @@ public void Concurrent_Hits_Do_Not_Apply_After_Pause_Or_Finish() AssertHitsDoNotApplyAfterControl(pause: false); } + private static void AssertWatchDoesNotResumeAfterControl(bool pause) + { + var hub = new CodeStatHub(); + var session = hub.StartSession(); + var entry = new CodeStatEntry("script.os", "Method", 1); + hub.MarkEntryReached(entry, 0); + hub.MarkPrepared("script.os"); + hub.MarkEntryReached(entry); + + using var stopResuming = new ManualResetEventSlim(false); + var start = new Barrier(2); + + var resumer = Task.Run(() => + { + start.SignalAndWait(); + while (!stopResuming.IsSet) + hub.ResumeWatch(entry); + }); + + start.SignalAndWait(); + if (pause) + hub.PauseSession(session); + else + hub.FinishSession(session); + + var timeWhenControlReturned = TimeOf(session, entry); + Thread.Sleep(40); + stopResuming.Set(); + resumer.Wait(); + + TimeOf(session, entry).Should().Be(timeWhenControlReturned); + } + private static void AssertHitsDoNotApplyAfterControl(bool pause) { var hub = new CodeStatHub(); From 21566dfaf5dd27d63ce268994ccff2277d4652b8 Mon Sep 17 00:00:00 2001 From: Sergey Batanov Date: Thu, 3 Sep 2026 22:56:32 +0300 Subject: [PATCH 5/6] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=20=D0=B8=D1=81=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BD=D1=83=D0=BB=D0=B5=D0=B2=D1=8B=D1=85=20?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=BE=D0=BA=20=D0=B2=20=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D0=BE=D0=B4=D0=B0=D1=85=20=D1=81=D0=B1=D0=BE=D1=80=D0=B0=20?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=81=D1=82=D0=B8=D0=BA=D0=B8.?= =?UTF-8?q?=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20?= =?UTF-8?q?=D1=81=D0=BE=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D1=81=D1=82=D0=B2?= =?UTF-8?q?=D1=83=D1=8E=D1=89=D0=B8=D0=B5=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4?= =?UTF-8?q?=D1=8B=20=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BA=D0=BE=D1=80=D1=80=D0=B5=D0=BA=D1=82=D0=BD=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B8=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=BE=D0=B3=D0=BE=20=D1=84=D1=83=D0=BD=D0=BA=D1=86?= =?UTF-8?q?=D0=B8=D0=BE=D0=BD=D0=B0=D0=BB=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CodeStatisticsCollector.cs | 7 +-- .../Machine/CodeStat/CodeStatHub.cs | 7 ++- .../Machine/CodeStat/CodeStatProcessor.cs | 45 +++++++++++----- .../OneScript.Core.Tests/CodeStatHubTests.cs | 23 ++++++++ tests/codestat-api.os | 53 +++++++++++++++++++ 5 files changed, 116 insertions(+), 19 deletions(-) diff --git a/src/OneScript.StandardLibrary/CodeStatisticsCollector.cs b/src/OneScript.StandardLibrary/CodeStatisticsCollector.cs index 992adef68..df42602d1 100644 --- a/src/OneScript.StandardLibrary/CodeStatisticsCollector.cs +++ b/src/OneScript.StandardLibrary/CodeStatisticsCollector.cs @@ -53,15 +53,16 @@ public void Resume() _state = SessionState.Active; } + /// Истина — не включать в результат строки с нулевым количеством выполнений [ContextMethod("Завершить", "Finish")] - public ValueTable Finish() + public ValueTable Finish(bool excludeZeros = false) { if (_state == SessionState.Finished) ThrowInvalidState(); - _hub.FinishSession(_session); + _hub.FinishSession(_session, excludeZeros); _state = SessionState.Finished; - return ToValueTable(_session.GetStatData()); + return ToValueTable(_session.GetStatData(excludeZeros)); } private void EnsureState(SessionState expected) diff --git a/src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs b/src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs index 3be620393..756c47fb5 100644 --- a/src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs +++ b/src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs @@ -53,12 +53,15 @@ public void ResumeSession(CodeStatProcessor session) } } - public void FinishSession(CodeStatProcessor session) + public void FinishSession(CodeStatProcessor session, bool excludeZeros = false) { lock (_lock) { session.EndCodeStat(); - session.FreezeCatalog(_knownEntries.ToArray(), new HashSet(_preparedScripts)); + if (excludeZeros) + session.FinishWithoutCatalog(); + else + session.FreezeCatalog(_knownEntries.ToArray(), new HashSet(_preparedScripts)); _active = Remove(_active, session); _alive = Remove(_alive, session); } diff --git a/src/ScriptEngine/Machine/CodeStat/CodeStatProcessor.cs b/src/ScriptEngine/Machine/CodeStat/CodeStatProcessor.cs index 028ae38e4..da112945e 100644 --- a/src/ScriptEngine/Machine/CodeStat/CodeStatProcessor.cs +++ b/src/ScriptEngine/Machine/CodeStat/CodeStatProcessor.cs @@ -21,6 +21,7 @@ public class CodeStatProcessor : ICodeStatCollector private IReadOnlyList _frozenEntries; private int _frozenCount; private HashSet _frozenPrepared; + private bool _finishedHitsOnly; public CodeStatProcessor() { @@ -60,27 +61,18 @@ public void MarkPrepared(string scriptFileName) _preparedScripts.Add(scriptFileName); } - public CodeStatDataCollection GetStatData() + public CodeStatDataCollection GetStatData(bool excludeZeros = false) { + if (excludeZeros || _finishedHitsOnly) + return BuildFromHits(); + if (_frozenEntries != null) return BuildFromCatalog(_frozenEntries, _frozenCount, _frozenPrepared); if (_hub != null) return _hub.GetLiveStatData(this); - var data = new CodeStatDataCollection(); - foreach (var item in _codeStat) - { - if (!IsPrepared(item.Key.ScriptFileName)) - continue; - - long time = 0; - if (_watchers.TryGetValue(item.Key, out var watch)) - time = watch.ElapsedMilliseconds; - data.Add(new CodeStatData(item.Key, time, item.Value)); - } - - return data; + return BuildFromHits(requirePrepared: true, includeZeros: true); } internal void FreezeCatalog(CodeStatEntry[] entries, HashSet prepared) @@ -90,6 +82,11 @@ internal void FreezeCatalog(CodeStatEntry[] entries, HashSet prepared) _frozenPrepared = prepared; } + internal void FinishWithoutCatalog() + { + _finishedHitsOnly = true; + } + internal CodeStatDataCollection BuildFromCatalog( IReadOnlyList entries, int count, @@ -113,6 +110,26 @@ internal CodeStatDataCollection BuildFromCatalog( return data; } + private CodeStatDataCollection BuildFromHits(bool requirePrepared = false, bool includeZeros = false) + { + var data = new CodeStatDataCollection(); + foreach (var item in _codeStat) + { + if (!includeZeros && item.Value == 0) + continue; + if (requirePrepared && !IsPrepared(item.Key.ScriptFileName)) + continue; + + long time = 0; + if (_watchers.TryGetValue(item.Key, out var watch)) + time = watch.ElapsedMilliseconds; + + data.Add(new CodeStatData(item.Key, time, item.Value)); + } + + return data; + } + public void EndCodeStat() { StopActiveWatch(); diff --git a/src/Tests/OneScript.Core.Tests/CodeStatHubTests.cs b/src/Tests/OneScript.Core.Tests/CodeStatHubTests.cs index bbda928f5..af78d6e64 100644 --- a/src/Tests/OneScript.Core.Tests/CodeStatHubTests.cs +++ b/src/Tests/OneScript.Core.Tests/CodeStatHubTests.cs @@ -136,6 +136,29 @@ public void StartSession_Reads_Zeros_From_Shared_Catalog() data.Should().OnlyContain(x => x.ExecutionCount == 0 && x.TimeElapsed == 0); } + [Fact] + public void GetStatData_ExcludeZeros_Returns_Only_Hits() + { + var hub = new CodeStatHub(); + var session = hub.StartSession(); + var hit = new CodeStatEntry("script.os", "Method", 1); + var zero = new CodeStatEntry("script.os", "Method", 2); + + hub.MarkEntryReached(hit, 0); + hub.MarkEntryReached(zero, 0); + hub.MarkPrepared("script.os"); + hub.MarkEntryReached(hit); + + var live = session.GetStatData(true); + live.Should().ContainSingle(x => x.Entry.Equals(hit) && x.ExecutionCount == 1); + live.Should().NotContain(x => x.Entry.Equals(zero)); + + hub.FinishSession(session, excludeZeros: true); + var finished = session.GetStatData(true); + finished.Should().ContainSingle(x => x.Entry.Equals(hit) && x.ExecutionCount == 1); + finished.Should().NotContain(x => x.Entry.Equals(zero)); + } + [Fact] public void ResumeSession_Does_Not_Revive_Finished_Session() { diff --git a/tests/codestat-api.os b/tests/codestat-api.os index a779f736b..151354d4c 100644 --- a/tests/codestat-api.os +++ b/tests/codestat-api.os @@ -18,6 +18,7 @@ ВсеТесты.Добавить("ТестДолжен_ПроверитьПересекающиесяСборы"); ВсеТесты.Добавить("ТестДолжен_ПроверитьПаузуИВосстановление"); ВсеТесты.Добавить("ТестДолжен_ПроверитьПереходыСостояний"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьИсключениеНулевыхСтрок"); ВсеТесты.Добавить("ТестДолжен_ПроверитьДампВФайл"); Возврат ВсеТесты; @@ -118,6 +119,58 @@ КонецПроцедуры +Процедура ТестДолжен_ПроверитьИсключениеНулевыхСтрок() Экспорт + + ТекстСкрипта = + "Процедура ВыполняемыйМетод() + | Значение = 1; + |КонецПроцедуры + | + |Процедура НевызываемыйМетод() + | Значение = 2; + |КонецПроцедуры + | + |Функция КоличествоХитов(Таблица, ИмяМетода) + | Сумма = 0; + | Для Каждого СтрокаТаблицы Из Таблица Цикл + | Если СтрокаТаблицы.Метод = ИмяМетода Тогда + | Сумма = Сумма + СтрокаТаблицы.Количество; + | КонецЕсли; + | КонецЦикла; + | Возврат Сумма; + |КонецФункции + | + |Функция ЕстьСтрокиМетода(Таблица, ИмяМетода) + | Для Каждого СтрокаТаблицы Из Таблица Цикл + | Если СтрокаТаблицы.Метод = ИмяМетода Тогда + | Возврат Истина; + | КонецЕсли; + | КонецЦикла; + | Возврат Ложь; + |КонецФункции + | + |Сбор = СборСтатистики.НачатьСбор(); + |ВыполняемыйМетод(); + |Таблица = Сбор.Завершить(Истина); + | + |Если КоличествоХитов(Таблица, ""ВыполняемыйМетод"") = 0 Тогда + | ВызватьИсключение ""Хиты ВыполняемыйМетод должны остаться""; + |КонецЕсли; + | + |Если ЕстьСтрокиМетода(Таблица, ""НевызываемыйМетод"") Тогда + | ВызватьИсключение ""НевызываемыйМетод не должен попасть в таблицу при excludeZeros""; + |КонецЕсли; + | + |Для Каждого СтрокаТаблицы Из Таблица Цикл + | Если СтрокаТаблицы.Количество = 0 Тогда + | ВызватьИсключение ""В таблице не должно быть строк с Количество = 0""; + | КонецЕсли; + |КонецЦикла;"; + + ПроверитьУспешныйЗапуск(ТекстСкрипта); + +КонецПроцедуры + Процедура ТестДолжен_ПроверитьПересекающиесяСборы() Экспорт ТекстСкрипта = From a7a7d1d3c6d1eb65011d936e65eb09249f287dcd Mon Sep 17 00:00:00 2001 From: Sergey Batanov Date: Thu, 3 Sep 2026 23:23:43 +0300 Subject: [PATCH 6/6] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=20=D0=BD=D0=BE=D0=B2=D0=BE=D0=B5=20=D0=B2=D0=BD?= =?UTF-8?q?=D1=83=D1=82=D1=80=D0=B5=D0=BD=D0=BD=D0=B5=D0=B5=20=D1=81=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=B4=D0=B3=D0=BE=D1=82=D0=BE=D0=B2=D0=BA=D0=B8?= =?UTF-8?q?=20=D1=81=D0=BA=D1=80=D0=B8=D0=BF=D1=82=D0=BE=D0=B2=20=D0=B8=20?= =?UTF-8?q?=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BC?= =?UTF-8?q?=D0=B5=D1=82=D0=BE=D0=B4=D1=8B=20=D1=81=D0=B1=D0=BE=D1=80=D0=B0?= =?UTF-8?q?=20=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=81=D1=82=D0=B8=D0=BA=D0=B8?= =?UTF-8?q?,=20=D1=87=D1=82=D0=BE=D0=B1=D1=8B=20=D1=83=D1=87=D0=B8=D1=82?= =?UTF-8?q?=D1=8B=D0=B2=D0=B0=D1=82=D1=8C=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=BF=D0=BE=D0=B4=D0=B3=D0=BE=D1=82=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BD=D1=8B=D0=B5=20=D1=81=D0=BA=D1=80=D0=B8=D0=BF?= =?UTF-8?q?=D1=82=D1=8B.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BA=D0=BE=D1=80=D1=80=D0=B5=D0=BA=D1=82=D0=BD=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B8=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=BE=D0=B3=D0=BE=20=D1=84=D1=83=D0=BD=D0=BA=D1=86?= =?UTF-8?q?=D0=B8=D0=BE=D0=BD=D0=B0=D0=BB=D0=B0,=20=D0=B8=D1=81=D0=BA?= =?UTF-8?q?=D0=BB=D1=8E=D1=87=D0=B0=D1=8E=D1=89=D0=B5=D0=B3=D0=BE=20=D0=BD?= =?UTF-8?q?=D1=83=D0=BB=D0=B5=D0=B2=D1=8B=D0=B5=20=D0=B7=D0=BD=D0=B0=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Machine/CodeStat/CodeStatHub.cs | 8 ++++ .../Machine/CodeStat/CodeStatProcessor.cs | 15 +++++-- .../OneScript.Core.Tests/CodeStatHubTests.cs | 40 +++++++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs b/src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs index 756c47fb5..416e2f8e5 100644 --- a/src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs +++ b/src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs @@ -83,6 +83,14 @@ public bool IsPrepared(string ScriptFileName) } } + internal HashSet SnapshotPreparedScripts() + { + lock (_lock) + { + return new HashSet(_preparedScripts); + } + } + public void MarkEntryReached(CodeStatEntry entry, int count = 1) { lock (_lock) diff --git a/src/ScriptEngine/Machine/CodeStat/CodeStatProcessor.cs b/src/ScriptEngine/Machine/CodeStat/CodeStatProcessor.cs index da112945e..d0cb1c526 100644 --- a/src/ScriptEngine/Machine/CodeStat/CodeStatProcessor.cs +++ b/src/ScriptEngine/Machine/CodeStat/CodeStatProcessor.cs @@ -64,7 +64,7 @@ public void MarkPrepared(string scriptFileName) public CodeStatDataCollection GetStatData(bool excludeZeros = false) { if (excludeZeros || _finishedHitsOnly) - return BuildFromHits(); + return BuildFromHits(requirePrepared: true, prepared: PreparedScriptsForHits()); if (_frozenEntries != null) return BuildFromCatalog(_frozenEntries, _frozenCount, _frozenPrepared); @@ -75,6 +75,11 @@ public CodeStatDataCollection GetStatData(bool excludeZeros = false) return BuildFromHits(requirePrepared: true, includeZeros: true); } + private HashSet PreparedScriptsForHits() + { + return _hub != null ? _hub.SnapshotPreparedScripts() : _preparedScripts; + } + internal void FreezeCatalog(CodeStatEntry[] entries, HashSet prepared) { _frozenEntries = entries; @@ -110,14 +115,18 @@ internal CodeStatDataCollection BuildFromCatalog( return data; } - private CodeStatDataCollection BuildFromHits(bool requirePrepared = false, bool includeZeros = false) + private CodeStatDataCollection BuildFromHits( + bool requirePrepared = false, + bool includeZeros = false, + HashSet prepared = null) { var data = new CodeStatDataCollection(); + var preparedSet = prepared ?? _preparedScripts; foreach (var item in _codeStat) { if (!includeZeros && item.Value == 0) continue; - if (requirePrepared && !IsPrepared(item.Key.ScriptFileName)) + if (requirePrepared && !preparedSet.Contains(item.Key.ScriptFileName)) continue; long time = 0; diff --git a/src/Tests/OneScript.Core.Tests/CodeStatHubTests.cs b/src/Tests/OneScript.Core.Tests/CodeStatHubTests.cs index af78d6e64..d2def26f9 100644 --- a/src/Tests/OneScript.Core.Tests/CodeStatHubTests.cs +++ b/src/Tests/OneScript.Core.Tests/CodeStatHubTests.cs @@ -136,6 +136,46 @@ public void StartSession_Reads_Zeros_From_Shared_Catalog() data.Should().OnlyContain(x => x.ExecutionCount == 0 && x.TimeElapsed == 0); } + [Fact] + public void GetStatData_ExcludeZeros_Ignores_Hits_Before_Prepared() + { + var hub = new CodeStatHub(); + var session = hub.StartSession(); + var early = new CodeStatEntry("early.os", "Method", 1); + var ready = new CodeStatEntry("ready.os", "Method", 1); + + hub.MarkEntryReached(early); + hub.MarkPrepared("ready.os"); + hub.MarkEntryReached(ready); + + session.GetStatData().Should().NotContain(x => x.Entry.Equals(early)); + session.GetStatData().Should().ContainSingle(x => x.Entry.Equals(ready) && x.ExecutionCount == 1); + + var withoutZeros = session.GetStatData(true); + withoutZeros.Should().NotContain(x => x.Entry.Equals(early)); + withoutZeros.Should().ContainSingle(x => x.Entry.Equals(ready) && x.ExecutionCount == 1); + + hub.FinishSession(session, excludeZeros: true); + var finished = session.GetStatData(true); + finished.Should().NotContain(x => x.Entry.Equals(early)); + finished.Should().ContainSingle(x => x.Entry.Equals(ready) && x.ExecutionCount == 1); + } + + [Fact] + public void Standalone_GetStatData_ExcludeZeros_Ignores_Hits_Before_Prepared() + { + var processor = new CodeStatProcessor(); + var early = new CodeStatEntry("early.os", "Method", 1); + + processor.MarkEntryReached(early); + + processor.GetStatData().Should().BeEmpty(); + processor.GetStatData(true).Should().BeEmpty(); + + processor.MarkPrepared("early.os"); + processor.GetStatData(true).Should().ContainSingle(x => x.Entry.Equals(early) && x.ExecutionCount == 1); + } + [Fact] public void GetStatData_ExcludeZeros_Returns_Only_Hits() {