diff --git a/src/OneScript.StandardLibrary/CodeStatisticsCollector.cs b/src/OneScript.StandardLibrary/CodeStatisticsCollector.cs new file mode 100644 index 000000000..df42602d1 --- /dev/null +++ b/src/OneScript.StandardLibrary/CodeStatisticsCollector.cs @@ -0,0 +1,106 @@ +/*---------------------------------------------------------- +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(bool excludeZeros = false) + { + if (_state == SessionState.Finished) + ThrowInvalidState(); + + _hub.FinishSession(_session, excludeZeros); + _state = SessionState.Finished; + return ToValueTable(_session.GetStatData(excludeZeros)); + } + + 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..416e2f8e5 --- /dev/null +++ b/src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs @@ -0,0 +1,159 @@ +/*---------------------------------------------------------- +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 List _knownEntries = new List(); + private readonly HashSet _knownSet = 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(this); + lock (_lock) + { + _alive = Append(_alive, session); + _active = Append(_active, session); + } + + return session; + } + + public void PauseSession(CodeStatProcessor session) + { + lock (_lock) + { + session.StopActiveWatch(); + _active = Remove(_active, session); + } + } + + 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); + } + } + + public void FinishSession(CodeStatProcessor session, bool excludeZeros = false) + { + lock (_lock) + { + session.EndCodeStat(); + if (excludeZeros) + session.FinishWithoutCatalog(); + else + 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) + { + return _preparedScripts.Contains(ScriptFileName); + } + } + + internal HashSet SnapshotPreparedScripts() + { + lock (_lock) + { + return new HashSet(_preparedScripts); + } + } + + public void MarkEntryReached(CodeStatEntry entry, int count = 1) + { + lock (_lock) + { + 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) + { + lock (_lock) + { + _preparedScripts.Add(scriptFileName); + } + } + + public void StopWatch(CodeStatEntry entry) + { + lock (_lock) + { + foreach (var session in _active) + session.StopWatch(entry); + } + } + + public void ResumeWatch(CodeStatEntry entry) + { + lock (_lock) + { + foreach (var session in _active) + session.ResumeWatch(entry); + } + } + + 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..d0cb1c526 100644 --- a/src/ScriptEngine/Machine/CodeStat/CodeStatProcessor.cs +++ b/src/ScriptEngine/Machine/CodeStat/CodeStatProcessor.cs @@ -1,21 +1,36 @@ /*---------------------------------------------------------- -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; + private bool _finishedHitsOnly; + + public CodeStatProcessor() + { + } + + internal CodeStatProcessor(CodeStatHub hub) + { + _hub = hub; + } public bool IsPrepared(string ScriptFileName) { @@ -24,23 +39,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(); - _activeStopwatch = _watchers[entry]; - _activeStopwatch.Start(); + watch = new Stopwatch(); + _watchers[entry] = watch; } + + _activeStopwatch = watch; + _activeStopwatch.Start(); } public void MarkPrepared(string scriptFileName) @@ -48,41 +61,108 @@ public void MarkPrepared(string scriptFileName) _preparedScripts.Add(scriptFileName); } - public CodeStatDataCollection GetStatData() + public CodeStatDataCollection GetStatData(bool excludeZeros = false) + { + if (excludeZeros || _finishedHitsOnly) + return BuildFromHits(requirePrepared: true, prepared: PreparedScriptsForHits()); + + if (_frozenEntries != null) + return BuildFromCatalog(_frozenEntries, _frozenCount, _frozenPrepared); + + if (_hub != null) + return _hub.GetLiveStatData(this); + + return BuildFromHits(requirePrepared: true, includeZeros: true); + } + + private HashSet PreparedScriptsForHits() + { + return _hub != null ? _hub.SnapshotPreparedScripts() : _preparedScripts; + } + + internal void FreezeCatalog(CodeStatEntry[] entries, HashSet prepared) { - CodeStatDataCollection data = new CodeStatDataCollection(); + _frozenEntries = entries; + _frozenCount = entries.Length; + _frozenPrepared = prepared; + } + + internal void FinishWithoutCatalog() + { + _finishedHitsOnly = true; + } + + 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; + } + + 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 (!IsPrepared(item.Key.ScriptFileName)) - { + if (!includeZeros && item.Value == 0) continue; - } - data.Add(new CodeStatData(item.Key, _watchers[item.Key].ElapsedMilliseconds, item.Value)); + if (requirePrepared && !preparedSet.Contains(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(); + } + + public void StopActiveWatch() { _activeStopwatch?.Stop(); + _activeStopwatch = null; } 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 new file mode 100644 index 000000000..d2def26f9 --- /dev/null +++ b/src/Tests/OneScript.Core.Tests/CodeStatHubTests.cs @@ -0,0 +1,313 @@ +/*---------------------------------------------------------- +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.Linq; +using System.Threading; +using System.Threading.Tasks; +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); + } + + [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 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() + { + 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() + { + 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() + { + AssertHitsDoNotApplyAfterControl(pause: true); + 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(); + 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; + } + } +} 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..151354d4c --- /dev/null +++ b/tests/codestat-api.os @@ -0,0 +1,412 @@ +/////////////////////////////////////////////////////////////////////// +// +// Сбор статистики из исполняемого кода +// +/////////////////////////////////////////////////////////////////////// + +Перем юТест; + +Функция ПолучитьСписокТестов(ЮнитТестирование) Экспорт + + юТест = ЮнитТестирование; + + ВсеТесты = Новый Массив; + ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоСборСтатистикиЕстьВГлобальномКонтексте"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоСборНедоступенБезФлага"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоНачатьСборБросаетБезФлага"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьСборДоступенИРезультатЗавершения"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьПересекающиесяСборы"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьПаузуИВосстановление"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьПереходыСостояний"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьИсключениеНулевыхСтрок"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьДампВФайл"); + + Возврат ВсеТесты; + +КонецФункции + +Процедура ТестДолжен_ПроверитьЧтоСборСтатистикиЕстьВГлобальномКонтексте() Экспорт + + юТест.ПроверитьРавенство(Тип("СборСтатистики"), ТипЗнч(СборСтатистики)); + юТест.ПроверитьЛожь(СборСтатистики.СборДоступен()); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьЧтоСборНедоступенБезФлага() Экспорт + + юТест.ПроверитьЛожь(СборСтатистики.СборДоступен(), "Без -codestat сбор должен быть недоступен"); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьЧтоНачатьСборБросаетБезФлага() Экспорт + + Бросило = Ложь; + Попытка + СборСтатистики.НачатьСбор(); + Исключение + Бросило = Истина; + юТест.ПроверитьИстину( + СтрНайти(ИнформацияОбОшибке().Описание, "-codestat") > 0, + "Текст исключения должен упоминать -codestat" + ); + КонецПопытки; + + юТест.ПроверитьИстину(Бросило, "НачатьСбор без -codestat должен выбрасывать исключение"); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьСборДоступенИРезультатЗавершения() Экспорт + + ТекстСкрипта = + "Процедура ВыполняемыйМетод() + | Значение = 1; + |КонецПроцедуры + | + |Процедура НевызываемыйМетод() + | Значение = 2; + |КонецПроцедуры + | + |Функция КоличествоХитов(Таблица, ИмяМетода) + | Сумма = 0; + | Для Каждого СтрокаТаблицы Из Таблица Цикл + | Если СтрокаТаблицы.Метод = ИмяМетода Тогда + | Сумма = Сумма + СтрокаТаблицы.Количество; + | КонецЕсли; + | КонецЦикла; + | Возврат Сумма; + |КонецФункции + | + |Функция ВсеСтрокиМетодаСНулями(Таблица, ИмяМетода) + | Нашли = Ложь; + | Для Каждого СтрокаТаблицы Из Таблица Цикл + | Если СтрокаТаблицы.Метод = ИмяМетода Тогда + | Нашли = Истина; + | Если СтрокаТаблицы.Количество <> 0 Тогда + | Возврат Ложь; + | КонецЕсли; + | КонецЕсли; + | КонецЦикла; + | Возврат Нашли; + |КонецФункции + | + |Если Не СборСтатистики.СборДоступен() Тогда + | ВызватьИсключение ""Сбор должен быть доступен""; + |КонецЕсли; + | + |Сбор = СборСтатистики.НачатьСбор(); + |ВыполняемыйМетод(); + |Таблица = Сбор.Завершить(); + | + |Если ТипЗнч(Таблица) <> Тип(""ТаблицаЗначений"") Тогда + | ВызватьИсключение ""Завершить должен вернуть ТаблицаЗначений""; + |КонецЕсли; + | + |Для Каждого ИмяКолонки Из СтрРазделить(""Путь,Метод,НомерСтроки,Количество,Время"", "","") Цикл + | Если Таблица.Колонки.Найти(ИмяКолонки) = Неопределено Тогда + | ВызватьИсключение ""Нет колонки "" + ИмяКолонки; + | КонецЕсли; + |КонецЦикла; + | + |Если КоличествоХитов(Таблица, ""ВыполняемыйМетод"") = 0 Тогда + | ВызватьИсключение ""В результате должны быть хиты ВыполняемыйМетод""; + |КонецЕсли; + | + |Если Не ВсеСтрокиМетодаСНулями(Таблица, ""НевызываемыйМетод"") Тогда + | ВызватьИсключение ""Завершить должен вернуть непройденные строки НевызываемыйМетод с Количество = 0""; + |КонецЕсли;"; + + ПроверитьУспешныйЗапуск(ТекстСкрипта); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьИсключениеНулевыхСтрок() Экспорт + + ТекстСкрипта = + "Процедура ВыполняемыйМетод() + | Значение = 1; + |КонецПроцедуры + | + |Процедура НевызываемыйМетод() + | Значение = 2; + |КонецПроцедуры + | + |Функция КоличествоХитов(Таблица, ИмяМетода) + | Сумма = 0; + | Для Каждого СтрокаТаблицы Из Таблица Цикл + | Если СтрокаТаблицы.Метод = ИмяМетода Тогда + | Сумма = Сумма + СтрокаТаблицы.Количество; + | КонецЕсли; + | КонецЦикла; + | Возврат Сумма; + |КонецФункции + | + |Функция ЕстьСтрокиМетода(Таблица, ИмяМетода) + | Для Каждого СтрокаТаблицы Из Таблица Цикл + | Если СтрокаТаблицы.Метод = ИмяМетода Тогда + | Возврат Истина; + | КонецЕсли; + | КонецЦикла; + | Возврат Ложь; + |КонецФункции + | + |Сбор = СборСтатистики.НачатьСбор(); + |ВыполняемыйМетод(); + |Таблица = Сбор.Завершить(Истина); + | + |Если КоличествоХитов(Таблица, ""ВыполняемыйМетод"") = 0 Тогда + | ВызватьИсключение ""Хиты ВыполняемыйМетод должны остаться""; + |КонецЕсли; + | + |Если ЕстьСтрокиМетода(Таблица, ""НевызываемыйМетод"") Тогда + | ВызватьИсключение ""НевызываемыйМетод не должен попасть в таблицу при excludeZeros""; + |КонецЕсли; + | + |Для Каждого СтрокаТаблицы Из Таблица Цикл + | Если СтрокаТаблицы.Количество = 0 Тогда + | ВызватьИсключение ""В таблице не должно быть строк с Количество = 0""; + | КонецЕсли; + |КонецЦикла;"; + + ПроверитьУспешныйЗапуск(ТекстСкрипта); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьПересекающиесяСборы() Экспорт + + ТекстСкрипта = + "Процедура ТолькоА() + | Значение = 1; + |КонецПроцедуры + | + |Процедура ОбщийКод() + | Значение = 2; + |КонецПроцедуры + | + |Функция КоличествоХитов(Таблица, ИмяМетода) + | Сумма = 0; + | Для Каждого СтрокаТаблицы Из Таблица Цикл + | Если СтрокаТаблицы.Метод = ИмяМетода Тогда + | Сумма = Сумма + СтрокаТаблицы.Количество; + | КонецЕсли; + | КонецЦикла; + | Возврат Сумма; + |КонецФункции + | + |Функция ВсеСтрокиМетодаСНулями(Таблица, ИмяМетода) + | Нашли = Ложь; + | Для Каждого СтрокаТаблицы Из Таблица Цикл + | Если СтрокаТаблицы.Метод = ИмяМетода Тогда + | Нашли = Истина; + | Если СтрокаТаблицы.Количество <> 0 Тогда + | Возврат Ложь; + | КонецЕсли; + | КонецЕсли; + | КонецЦикла; + | Возврат Нашли; + |КонецФункции + | + |СборА = СборСтатистики.НачатьСбор(); + |ТолькоА(); + | + |СборБ = СборСтатистики.НачатьСбор(); + |ОбщийКод(); + | + |ТаблицаБ = СборБ.Завершить(); + |ТолькоА(); + |ТаблицаА = СборА.Завершить(); + | + |Если КоличествоХитов(ТаблицаА, ""ТолькоА"") = 0 Тогда + | ВызватьИсключение ""Сбор А должен содержать ТолькоА""; + |КонецЕсли; + |Если Не ВсеСтрокиМетодаСНулями(ТаблицаБ, ""ТолькоА"") Тогда + | ВызватьИсключение ""Сбор Б должен содержать непройденные строки ТолькоА с Количество = 0""; + |КонецЕсли; + |Если КоличествоХитов(ТаблицаА, ""ОбщийКод"") = 0 Тогда + | ВызватьИсключение ""Сбор А должен содержать ОбщийКод""; + |КонецЕсли; + |Если КоличествоХитов(ТаблицаБ, ""ОбщийКод"") = 0 Тогда + | ВызватьИсключение ""Сбор Б должен содержать ОбщийКод""; + |КонецЕсли;"; + + ПроверитьУспешныйЗапуск(ТекстСкрипта); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьПаузуИВосстановление() Экспорт + + ТекстСкрипта = + "Процедура ДоПаузы() + | Значение = 1; + |КонецПроцедуры + | + |Процедура НаПаузе() + | Значение = 2; + |КонецПроцедуры + | + |Процедура ПослеПаузы() + | Значение = 3; + |КонецПроцедуры + | + |Функция КоличествоХитов(Таблица, ИмяМетода) + | Сумма = 0; + | Для Каждого СтрокаТаблицы Из Таблица Цикл + | Если СтрокаТаблицы.Метод = ИмяМетода Тогда + | Сумма = Сумма + СтрокаТаблицы.Количество; + | КонецЕсли; + | КонецЦикла; + | Возврат Сумма; + |КонецФункции + | + |Функция ВсеСтрокиМетодаСНулями(Таблица, ИмяМетода) + | Нашли = Ложь; + | Для Каждого СтрокаТаблицы Из Таблица Цикл + | Если СтрокаТаблицы.Метод = ИмяМетода Тогда + | Нашли = Истина; + | Если СтрокаТаблицы.Количество <> 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...]