world timer supports numa and thread affinity binding

This commit is contained in:
Youmu Liu
2026-08-20 16:53:34 +08:00
parent 843eb6b237
commit 968a6afe1a
19 changed files with 223 additions and 88 deletions
@@ -6,9 +6,11 @@
// -----------------------------------------------------------------------------
using System;
using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Threading;
namespace StgSharp.Collections
@@ -122,6 +124,24 @@ namespace StgSharp.Collections
_index += span.Length;
}
public void PushRange(
SpanAction<T, int> filling,
int count
)
{
if (count == 0) {
return;
}
if (_index + count > _values.Length) {
throw new InvalidOperationException("Not enough space in the stack to push the entire span.");
}
if (filling is not null)
{
filling(_values.AsSpan()[_index..(_index + count)], count);
_index += count;
}
}
public bool TryPop(
out T value
)
@@ -163,4 +183,4 @@ namespace StgSharp.Collections
}
}
}
}
@@ -21,8 +21,6 @@ namespace StgSharp
internal const int ssdSegmentLength = 16;
private static byte[] _currentHash = [];
private static bool _inited;
public static byte[] CurrentAssemblyHash
@@ -32,12 +30,10 @@ namespace StgSharp
if (_currentHash == null)
{
string route = Assembly.GetExecutingAssembly().Location;
string time = DateTime.UtcNow.ToString();
using (SHA256 sh = SHA256.Create())
{
byte[] bytes = Encoding.UTF8.GetBytes(route);
_currentHash = sh.ComputeHash(bytes);
}
// string time = DateTime.UtcNow.ToString();
byte[] bytes = Encoding.UTF8.GetBytes(route);
_currentHash = SHA256.HashData(bytes);
}
return _currentHash;
}
@@ -52,6 +48,8 @@ namespace StgSharp
internal set => _mainThreadID = value;
}
private static byte[] _currentHash { get; set; } = [];
private static int _mainThreadID { get; set; } = -1;
public static class GlobalSetting
@@ -62,12 +60,10 @@ namespace StgSharp
public static bool VSyncActivated
{
get => vsyncActivated;
set =>
// GraphicFramework.glfwSwapInterval(value ? 1 : 0);
vsyncActivated = value;
set => vsyncActivated = value; // GraphicFramework.glfwSwapInterval(value ? 1 : 0);
}
}
}//-------------------------------------- End of Class ---------------------------------------//
}
}
@@ -27,11 +27,11 @@ namespace StgSharp
/// Performs all initialization work for this module using the provided profile.
/// </summary>
/// <param name="profile">
/// The configuration profile supplied at registration time. Use <see
/// cref="IModuleInitializeProfile.Empty" /> if no configuration is required.
/// The configuration profile supplied at registration time. Use <see ///
/// cref="IModuleInitializeConfiguration.Empty" /> if no configuration is required.
/// </param>
void InitializeModule(
IModuleInitializeProfile profile
IModuleInitializeConfiguration profile
);
/// <summary>
@@ -46,15 +46,15 @@ namespace StgSharp
/// Marker interface for module initialization configuration. Implementations are plain data
/// carriers; no static factory is required.
/// </summary>
public interface IModuleInitializeProfile
public interface IModuleInitializeConfiguration
{
/// <summary>
/// A shared, allocation-free empty profile for modules that require no configuration.
/// </summary>
static readonly IModuleInitializeProfile Empty = EmptyModuleInitializeProfile.Instance;
static readonly IModuleInitializeConfiguration Empty = EmptyModuleInitializeProfile.Instance;
private sealed class EmptyModuleInitializeProfile : IModuleInitializeProfile
private sealed class EmptyModuleInitializeProfile : IModuleInitializeConfiguration
{
public static readonly EmptyModuleInitializeProfile Instance = new();
@@ -66,8 +66,8 @@ namespace StgSharp
}
/// <summary>
/// Pairs an <see cref="IStaticModule" /> with its <see cref="IModuleInitializeProfile" />,
/// ensuring the two are never stored or iterated independently.
/// Pairs an <see cref="IStaticModule" /> with its <see cref="IModuleInitializeConfiguration"
/// />, ensuring the two are never stored or iterated independently.
/// </summary>
/// <remarks>
/// Using a <see langword="readonly struct" /> avoids the boxing overhead that would occur if
@@ -77,20 +77,20 @@ namespace StgSharp
{
/// <summary>
/// Initializes a new <see cref="ModuleDescriptor" /> using <see
/// cref="IModuleInitializeProfile.Empty" /> as the profile.
/// Initializes a new <see cref="ModuleDescriptor" /> using <see ///
/// cref="IModuleInitializeConfiguration.Empty" /> as the profile.
/// </summary>
public ModuleDescriptor(
IStaticModule module
)
: this(module, IModuleInitializeProfile.Empty) { }
: this(module, IModuleInitializeConfiguration.Empty) { }
/// <summary>
/// Initializes a new <see cref="ModuleDescriptor" /> with an explicit profile.
/// </summary>
public ModuleDescriptor(
IStaticModule module,
IModuleInitializeProfile profile
IModuleInitializeConfiguration profile
)
{
Module = module;
@@ -100,7 +100,7 @@ namespace StgSharp
/// <summary>
/// Gets the configuration profile associated with the module.
/// </summary>
public IModuleInitializeProfile Profile { get; }
public IModuleInitializeConfiguration Profile { get; }
/// <summary>
/// Gets the module to be initialized.
@@ -127,8 +127,8 @@ namespace StgSharp
/// <summary>
/// An ordered collection of modules to be initialized during application startup and
/// uninitialized on shutdown. Supports three registration strategies so that the <c> new()
/// </c> generic constraint is never a hard requirement.
/// uninitialized on shutdown. Supports three registration strategies so that the <c>new()
/// ///</c> generic constraint is never a hard requirement.
/// </summary>
public sealed class ModuleToInitializeCollection : IEnumerable<IStaticModule>, IStaticModule
{
@@ -136,10 +136,11 @@ namespace StgSharp
private readonly List<ModuleDescriptor> _descriptors = [];
internal ModuleToInitializeCollection(
World.Initializer i
World.Initializer init,
World.InitializeConfiguration config
)
{
_descriptors.Add(new ModuleDescriptor(i));
_descriptors.Add(new ModuleDescriptor(init, config));
}
// ── IEnumerable<IStaticModule> ───────────────────────────────────────────
@@ -189,11 +190,11 @@ namespace StgSharp
/// public parameterless constructor.
/// </typeparam>
/// <param name="profile">
/// Optional initialization profile. Defaults to <see
/// cref="IModuleInitializeProfile.Empty" /> when <see langword="null" />.
/// Optional initialization profile. Defaults to <see ///
/// cref="IModuleInitializeConfiguration.Empty" /> when <see langword="null" />.
/// </param>
public ModuleToInitializeCollection UseModule<T>(
IModuleInitializeProfile? profile = null
IModuleInitializeConfiguration? profile = null
) where T : IStaticModule, new()
{
return UseModule(static() => new T(), profile);
@@ -206,17 +207,17 @@ namespace StgSharp
/// </summary>
/// <param name="factory">
/// A delegate that constructs and returns the module instance. The delegate is invoked
/// once, immediately at registration time. If <paramref name="factory" /> is <see
/// once, immediately at registration time. If <paramref name="factory" /> is <see ///
/// langword="null" /> or returns <see langword="null" />, the call is silently ignored
/// and the collection is returned unchanged.
/// </param>
/// <param name="profile">
/// Optional initialization profile. Defaults to <see
/// cref="IModuleInitializeProfile.Empty" /> when <see langword="null" />.
/// Optional initialization profile. Defaults to <see ///
/// cref="IModuleInitializeConfiguration.Empty" /> when <see langword="null" />.
/// </param>
public ModuleToInitializeCollection UseModule(
Func<IStaticModule> factory,
IModuleInitializeProfile? profile = null
IModuleInitializeConfiguration? profile = null
)
{
if (factory is null) {
@@ -228,7 +229,7 @@ namespace StgSharp
}
_descriptors.Add(new ModuleDescriptor(
mod,
profile ?? IModuleInitializeProfile.Empty));
profile ?? IModuleInitializeConfiguration.Empty));
return this;
}
@@ -240,17 +241,17 @@ namespace StgSharp
/// The module instance to register.
/// </param>
/// <param name="profile">
/// Optional initialization profile. Defaults to <see
/// cref="IModuleInitializeProfile.Empty" /> when <see langword="null" />.
/// Optional initialization profile. Defaults to <see ///
/// cref="IModuleInitializeConfiguration.Empty" /> when <see langword="null" />.
/// </param>
public ModuleToInitializeCollection UseModule(
IStaticModule module,
IModuleInitializeProfile? profile = null
IModuleInitializeConfiguration? profile = null
)
{
_descriptors.Add(new ModuleDescriptor(
module,
profile ?? IModuleInitializeProfile.Empty));
profile ?? IModuleInitializeConfiguration.Empty));
return this;
}
@@ -263,12 +264,12 @@ namespace StgSharp
// ── IStaticModule explicit implementation ────────────────────────────────
/// <summary>
/// Explicit <see cref="IStaticModule" /> implementation. Delegates to <see
/// Explicit <see cref="IStaticModule" /> implementation. Delegates to <see ///
/// cref="InitializeModule()" />, ignoring the supplied profile because each contained module uses
/// its own registered profile.
/// </summary>
void IStaticModule.InitializeModule(
IModuleInitializeProfile profile
IModuleInitializeConfiguration profile
)
{
InitializeModule();
@@ -278,4 +279,4 @@ namespace StgSharp
string IStaticModule.ModuleName => "Module Initializer";
}
}
}
+21 -6
View File
@@ -7,6 +7,7 @@
using StgSharp.HighPerformance;
using StgSharp.HighPerformance.ProcessorAbstraction;
using StgSharp.Timing;
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
@@ -36,9 +37,11 @@ namespace StgSharp
private static uint markCount = 0;
internal static GraphicAPI API = default;
public static ModuleToInitializeCollection Initialize()
public static ModuleToInitializeCollection Initialize(
InitializeConfiguration config
)
{
return new ModuleToInitializeCollection(new Initializer());
return new ModuleToInitializeCollection(new Initializer(), config);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -177,10 +180,12 @@ namespace StgSharp
}
}
internal static unsafe void InternalInitialize()
internal static unsafe void InternalInitialize(
InitializeConfiguration config
)
{
// Initialize time provider and start providing time
MainTimeProvider.StartProvidingTime();
MainTimeProvider.StartProvidingTime(config.TimingConfig);
DefaultLog.InternalAppendLog("\n\n\n");
DefaultLog.InternalWriteLog(
$"Program {Assembly.GetEntryAssembly()!.FullName} on {Environment.MachineName} Started.", LogType.Info);
@@ -197,16 +202,26 @@ namespace StgSharp
/**/
}
public class InitializeConfiguration : IModuleInitializeConfiguration
{
public TimeSourceStartupConfiguration TimingConfig { get; set; }
}
internal class Initializer : IStaticModule
{
public string ModuleName => "StgSharpCore";
public void InitializeModule(
IModuleInitializeProfile profile
IModuleInitializeConfiguration config
)
{
World.InternalInitialize();
if (config is not InitializeConfiguration world_init) {
throw new InvalidCastException();
}
World.InternalInitialize(world_init);
}
public void UninitializeModule()
@@ -40,19 +40,19 @@ namespace StgSharp.HighPerformance.Memory
_buffer = buffer;
_Capacity = count;
int initCount = count switch
_recycle = new((int)_Capacity);
_recycle.PushRange(init, (int)count);
_currentIndex = _Capacity;
static void init(
Span<nuint> arr,
int _
)
{
<= 4 => 4,
> 64 => 64,
_ => (int)count
};
Span<nuint> span = stackalloc nuint[initCount];
for (int i = 0; i < initCount; i++) {
span[i] = _buffer + ((nuint)(i * _elementSize));
for (int i = 0; i < arr.Length; i++) {
arr[i] = (nuint)i;
}
}
_recycle = CapacityFixedStackBuilder.Create(span);
_Capacity = count;
_currentIndex = (nuint)initCount;
}
public override nuint Allocate()
@@ -123,7 +123,8 @@ namespace StgSharp.HighPerformance.ProcessorAbstraction
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
[DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)]
internal static partial void SetThreadAffinity(
int coreId
int coreId,
int numaId
);
[MethodImpl(MethodImplOptions.NoInlining)]
+13 -4
View File
@@ -6,6 +6,7 @@
// -----------------------------------------------------------------------------
using StgSharp.Collections;
using StgSharp.HighPerformance.ProcessorAbstraction;
using StgSharp.Timing;
using System;
using System.Diagnostics;
@@ -39,7 +40,9 @@ namespace StgSharp
return mainProvider.ElapsedTicks;
}
public sealed override void StartProvidingTime()
public sealed override void StartProvidingTime(
TimeSourceStartupConfiguration threadConfig
)
{
if (mainProvider.IsRunning) {
return;
@@ -49,9 +52,10 @@ namespace StgSharp
timeProvideThread = new Thread(ProvideTime)
{
IsBackground = true,
Name = "StgSharpTime"
Name = "StgSharpTime",
Priority = threadConfig.Priority,
};
timeProvideThread.Start();
timeProvideThread.Start(threadConfig);
}
public override void StopProvidingTime()
@@ -82,13 +86,18 @@ namespace StgSharp
_subscribers.Remove(subscriber);
}
private void ProvideTime()
private void ProvideTime(
object? param
)
{
TimeSourceStartupConfiguration threadConfig = (TimeSourceStartupConfiguration)param!;
long internalFrequency = Stopwatch.Frequency; // ticks per second
lock (mainProvider) {
mainProvider.Restart();
}
Thread.Sleep(10);
Numa.SetThreadAffinity((int)threadConfig.BindedCoreIndex, (int)threadConfig.BindedNumaNode);
while (mainProvider.IsRunning)
{
long elapsedTicks;
@@ -6,13 +6,28 @@
// -----------------------------------------------------------------------------
using StgSharp;
using StgSharp.HighPerformance.ProcessorAbstraction;
using System;
using System.Linq;
using System.Threading;
namespace StgSharp.Timing
{
public class TimeSourceStartupConfiguration(
nuint bindedNumaNode = 0,
nuint bindedCoreIndex = 0,
ThreadPriority threadPriority = ThreadPriority.Highest
)
{
public nuint BindedNumaNode { get; } = bindedNumaNode;
public nuint BindedCoreIndex { get; } = bindedCoreIndex;
public ThreadPriority Priority { get; } = threadPriority;
}
public abstract class TimeSourceProviderBase
{
@@ -40,7 +55,9 @@ namespace StgSharp.Timing
}
}
public abstract void StartProvidingTime();
public abstract void StartProvidingTime(
TimeSourceStartupConfiguration threadConfig
);
public abstract void StopProvidingTime();
@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace StgSharp.Common.Timing
{
internal class TimeThreadProfile
{
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ namespace StgSharp.Graphics
public string ModuleName => "Graphics";
public void InitializeModule(
IModuleInitializeProfile profile
IModuleInitializeConfiguration profile
)
{
GraphicFramework.LoadGlfw();
+1 -1
View File
@@ -20,7 +20,7 @@ namespace StgSharp.Graphics
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void InitializeModule(
IModuleInitializeProfile profile
IModuleInitializeConfiguration profile
)
{
GraphicFramework.LoadGlfw();
@@ -52,7 +52,7 @@ namespace StgSharp.Mathematics.Numeric
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void InitializeModule(
IModuleInitializeProfile profile
IModuleInitializeConfiguration profile
)
{
try
@@ -26,6 +26,8 @@ namespace StgSharp.RegularAnalysis.Text
AbstractSyntaxTree<RegexAstNode, RegexElementLabel> _tree = new();
RegexTokenReader reader = new(_source);
TokenParser<RegexElementLabel, RegexElementLabel> lexer = reader.Pipe(() => new RegexTokenParser());
Stack<RegexElementLabel> outerLastTokens = [];
RegexElementLabel lastToken = RegexElementLabel.NONE;
while (lexer.TryReadToken(out Token<RegexElementLabel> token))
{
@@ -38,8 +40,13 @@ namespace StgSharp.RegularAnalysis.Text
{
_stack.PushOperator(new RegexAstNode(token));
_stack.IncreaseDepth(0);
outerLastTokens.Push(lastToken);
lastToken = RegexElementLabel.GROUP_BEGIN;
} else if ((token.Flag & RegexElementLabel.GROUP_END) != 0)
{
if (lastToken == RegexElementLabel.ALT) {
PushEpsilon(token.Line, token.Column);
}
// close all nodes here
RegexAstNode op;
while (_stack.TryPopOperator(out op)) {
@@ -58,8 +65,17 @@ namespace StgSharp.RegularAnalysis.Text
op.Right = _stack.PopOperand();
_stack.PushOperand(op);
}
_ = outerLastTokens.Pop();
lastToken = RegexElementLabel.GROUP_END;
} else if ((token.Flag & RegexElementLabel.OPERATOR) != 0)
{
if (token.Flag == RegexElementLabel.ALT &&
lastToken is RegexElementLabel.NONE or
RegexElementLabel.GROUP_BEGIN or
RegexElementLabel.ALT)
{
PushEpsilon(token.Line, token.Column);
}
if (_stack.OperatorInDepthCount == 0)
{
// the first operator in stack
@@ -87,6 +103,13 @@ namespace StgSharp.RegularAnalysis.Text
_stack.PushOperator(new RegexAstNode(token));
}
}
if (token.Flag != RegexElementLabel.GROUP_BEGIN &&
token.Flag != RegexElementLabel.GROUP_END) {
lastToken = token.Flag;
}
}
if (lastToken == RegexElementLabel.ALT) {
PushEpsilon(0, _source.Length);
}
if (_stack.Depth == 1)
{
@@ -104,6 +127,13 @@ namespace StgSharp.RegularAnalysis.Text
}
throw new InvalidOperationException("Invalid regular expression syntax.");
void PushEpsilon(int row, int column)
{
RegexAstNode epsilon = new(new RegexEpsilonPayload(row, column));
_stack.PushOperand(epsilon);
_ = _tree.AddNode(epsilon);
}
void ProcessOperator(
RegexAstNode op
@@ -145,21 +145,22 @@ namespace StgSharp.RegularAnalysis.Text
TreeEnumerator<RegexAstNode, RegexElementLabel> enumerator = new(tree.Root);
RegexAstNode current;
// phase1: ALT flatten
// phase2: CONCAT normalize
int i = 0;
// phase1: normalize associative binary operators, then flatten ALT
List<RegexAstNode> altNodes = [];
while (enumerator.MoveNext())
{
current = enumerator.Current;
if (current.EqualityTypeConvert is RegexElementLabel.CONCAT or
RegexElementLabel.ALT)
{
RotateAssociativeOperator(current, current.EqualityTypeConvert);
}
if (current.EqualityTypeConvert == RegexElementLabel.ALT)
{
RegexAstNode union = FlattenAlt(current);
altNodes.Add(current);
current.Right = union;
current.Left = RegexAstNode.Empty;
} else if (current.EqualityTypeConvert == RegexElementLabel.CONCAT) {
RotateConcat(current);
}
}
@@ -195,13 +196,28 @@ namespace StgSharp.RegularAnalysis.Text
}
}
private static void RotateConcat(
RegexAstNode root
/// <summary>
/// Converts a left-associated binary operator tree to a right spine while preserving
/// operand order. For example, <c>(a op b) op c</c> becomes
/// <c>a op (b op c)</c>.
/// </summary>
/// <remarks>
/// A child with another label is an operand and is not rotated across. Keeping the target
/// label explicit allows later associative operators to reuse the same tree operation.
/// </remarks>
private static void RotateAssociativeOperator(
RegexAstNode root,
RegexElementLabel operatorLabel
)
{
if (root.EqualityTypeConvert != operatorLabel) {
throw new ArgumentException("The root does not match the requested operator.",
nameof(operatorLabel));
}
RegexAstNode left = root.Left;
while (!RegexAstNode.IsNullOrEmpty(left) &&
left.EqualityTypeConvert == RegexElementLabel.CONCAT)
left.EqualityTypeConvert == operatorLabel)
{
/*
* root → root
@@ -23,6 +23,15 @@ namespace StgSharp.RegularAnalysis.Text
public sealed record RegexEmptyPayload() : RegexAstPayload(string.Empty, 0, 0,
RegexElementLabel.NONE);
/// <summary>
/// A semantic empty expression. Unlike <see cref="RegexEmptyPayload"/>, this is a real AST
/// node and represents a successful match that consumes no input.
/// </summary>
public sealed record RegexEpsilonPayload(
int Row,
int Column
) : RegexAstPayload(string.Empty, Row, Column, RegexElementLabel.EPSILON);
/// <summary>
/// A deterministic literal sequence. Escapes represented as literals are consumed here.
/// </summary>
@@ -26,12 +26,13 @@ namespace StgSharp.RegularAnalysis.Text
COUNT = 32,
CONCAT = 64,
ALT = 128,
EPSILON = 256,
SEQUENCE = UNIT | UNIT_SPAN | UNIT_SET,
UNIT_SINGLE = UNIT | UNIT_SET,
OPERATOR = COUNT | ALT | CONCAT,
VAST_OPERATOR = OPERATOR | GROUP_BEGIN,
ATOM_BEGIN = SEQUENCE | GROUP_BEGIN ,
ATOM_END = SEQUENCE | GROUP_END | COUNT,
ATOM_BEGIN = SEQUENCE | GROUP_BEGIN | EPSILON,
ATOM_END = SEQUENCE | GROUP_END | COUNT | EPSILON,
#pragma warning restore CA1707
}
@@ -29,6 +29,9 @@ namespace StgSharp.RegularAnalysis.Text
)
{
RegexAstNode root = ast;
if (root.Label == RegexElementLabel.EPSILON) {
return;
}
RegexElementLabel op = root.Label & (RegexElementLabel.VAST_OPERATOR);
if (op != 0)
{