Skip to content
106 changes: 106 additions & 0 deletions src/OneScript.StandardLibrary/CodeStatisticsCollector.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Сессия сбора статистики исполнения кода. Создаётся через СборСтатистики.НачатьСбор().
/// </summary>
[ContextClass("СборщикСтатистикиКода", "CodeStatisticsCollector")]
public sealed class CodeStatisticsCollector : AutoContext<CodeStatisticsCollector>
{
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;
}

/// <param name="excludeZeros">Истина — не включать в результат строки с нулевым количеством выполнений</param>
[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;
}
}
}
42 changes: 42 additions & 0 deletions src/OneScript.StandardLibrary/CodeStatisticsContext.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Глобальный менеджер сбора статистики исполнения кода.
/// </summary>
[ContextClass("СборСтатистики", "CodeStatistics")]
public sealed class CodeStatisticsContext : AutoContext<CodeStatisticsContext>
{
[ContextMethod("СборДоступен", "CollectionAvailable")]
public bool CollectionAvailable(IBslProcess process)
{
return process.Services.TryResolve<ICodeStatCollector>() != null;
}

[ContextMethod("НачатьСбор", "StartCollection")]
public CodeStatisticsCollector StartCollection(IBslProcess process)
{
if (process.Services.TryResolve<ICodeStatCollector>() 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);
}
}
}
3 changes: 3 additions & 0 deletions src/ScriptEngine.HostedScript/HostedScriptEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ private void SetGlobalContexts(IGlobalsManager manager)

var bgTasksManager = new BackgroundTasksManager(_engine.Services.Resolve<ExecutionContext>());
_env.InjectGlobalProperty(bgTasksManager, "ФоновыеЗадания", "BackgroundJobs", true);

var codeStatistics = new CodeStatisticsContext();
_env.InjectGlobalProperty(codeStatistics, "СборСтатистики", "CodeStatistics", true);
}

public void Initialize()
Expand Down
159 changes: 159 additions & 0 deletions src/ScriptEngine/Machine/CodeStat/CodeStatHub.cs
Original file line number Diff line number Diff line change
@@ -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<CodeStatEntry> _knownEntries = new List<CodeStatEntry>();
private readonly HashSet<CodeStatEntry> _knownSet = new HashSet<CodeStatEntry>();
private readonly HashSet<string> _preparedScripts = new HashSet<string>();

private CodeStatProcessor[] _alive = Array.Empty<CodeStatProcessor>();
private CodeStatProcessor[] _active = Array.Empty<CodeStatProcessor>();

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<string>(_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<string> SnapshotPreparedScripts()
{
lock (_lock)
{
return new HashSet<string>(_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>(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>(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<T>();

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;
}
}
}
Loading