2
\$\begingroup\$

This class will fires a certain amount of ticks per second. I would greatly appreciate any suggentions, I'm more then welcome for any feedback since I haven't coded for long in C#.

It is intentional that if there is a heavy load on the ticks that the ticks per second don't match the desired input. It is more of a maximum were it should aim to run at, but because of that this class needs to be as performant as possible and with this we come to my first problem with this code:

I'm not really happy with the Thread.Sleep in the Pulse Method, which is my main concern, but could'nt figure out a better way. I needed it to reduce stress on the CPU on lower TPS..

Also the thing that this is using Time ticks to calculate the ticks for the engine makes it a little bit annouing to read but don't really know what to name it since both are ticks..

TickEngine.cs

using System;
using System.Threading.Tasks;
using System.Threading;
using System.Runtime.Serialization;

namespace TickEngine
{
    class TickEngine
    {
        public event EventHandler<TickEventArgs> OnStart = null!;
        public event EventHandler<TickEventArgs> OnPreTick = null!;
        public event EventHandler<TickEventArgs> OnTick = null!;
        public event EventHandler<TickEventArgs> OnPostTick = null!;
        public event EventHandler<TickEventArgs> OnStop = null!;

        private readonly int _resolution;
        private long _maxTick;
        private TimeSpan _delay;
        private ulong _tickCnt = 0;
        private float _deltaTickSum = 0;
        private bool _isEnabled = false;
        private long _timestamp = 0;
        private uint _tps;

        /// <summary>
        /// Is the Engine running
        /// </summary>
        public bool IsEnabled { get { return _isEnabled; } }

        /// <summary>
        /// Total ticks that have been processed since engine was started
        /// </summary>
        public ulong Tick { get { return _tickCnt; } }

        /// <summary>
        /// Ticks per Second
        /// </summary>
        public uint TPS
        {
            get { return _tps; }
            set
            {
                _tps = value;

                _maxTick = TimeSpan.TicksPerSecond / _tps;
                _delay = new TimeSpan(TimeSpan.TicksPerSecond / _tps / _resolution);
            }
        }

        /// <summary>
        ///  Initialzise the Tick engine
        /// </summary>
        /// <param name="tps">How many Ticks per Second should the Engine process</param>
        /// <param name="resolution">How many Checks per Tickinterval, higher number more CPU usage, but higher accuracy</param>
        public TickEngine(uint tps, int resolution = 3)
        {
            _resolution = resolution;
            TPS = tps;
        }

        /// <summary>
        /// Start the tick engine, need to handle own pulse loop
        /// </summary>
        /// <exception cref="TickEngineException">If engine is already running</exception>
        public void Start()
        {
            if (_isEnabled)
                throw new TickEngineException("Tick engine is already running");
            _tickCnt = 0;
            _deltaTickSum = 0;
            _timestamp = DateTime.Now.Ticks;
            _isEnabled = true;
            OnStart?.Invoke(this, new TickEventArgs { tick = _tickCnt });
        }

        /// <summary>
        /// Start the tick engine with async pulse loop
        /// </summary>
        /// <exception cref="TickEngineException">If engine is already running</exception>
        public void StartWithPulse()
        {
            if (_isEnabled)
                throw new TickEngineException("Tick engine is already running");
            Start();
            Task.Run(() =>
            {
                while (_isEnabled)
                {
                    Pulse();
                }
            });
        }

        /// <summary>
        /// Perform a pulse on the engine if enough time has passed a tick will be fired
        /// </summary>
        public void Pulse()
        {
            Update();
            Thread.Sleep(_delay);
        }

        /// <summary>
        /// Stop tick engine
        /// </summary>
        /// <exception cref="TickEngineException">Tick engine is Not running</exception>
        public void Stop()
        {
            if (!_isEnabled)
                throw new TickEngineException("Tick engine is NOT running");
            _isEnabled = false;
            OnStop?.Invoke(this, new TickEventArgs { tick = _tickCnt });
        }

        /// <summary>
        /// Update the timing and check if a tick(engine) will be fired.
        /// </summary>
        private void Update()
        {
            _deltaTickSum += DeltaTick();
            if (_deltaTickSum >= _maxTick)
            {
                _deltaTickSum -= _maxTick;
                _tickCnt++;
                TickEventArgs args = new TickEventArgs { tick = _tickCnt };

                OnPreTick?.Invoke(this, args);
                OnTick?.Invoke(this, args);
                OnPostTick?.Invoke(this, args);
            }
        }

        /// <summary>
        /// Calculate ticks(time) that have passed
        /// </summary>
        /// <returns>Returns the ticks(time) that have passed since last call</returns>
        private long DeltaTick()
        {
            long a = DateTime.Now.Ticks;
            long delta = a - _timestamp;
            _timestamp = a;
            return delta;
        }

        public class TickEventArgs : EventArgs
        {
            public ulong tick;
        }

        [Serializable]
        public class TickEngineException : Exception
        {
            public TickEngineException()
                : base()
            { }
            public TickEngineException(string message)
                : base(message)
            { }
            protected TickEngineException(SerializationInfo info, StreamingContext ctxt)
                : base(info, ctxt)
            { }
        }
    }
}

Example Program.cs

using System;
using System.Diagnostics;

namespace TickEngineExample // Note: actual namespace depends on the project name.
{
    internal class Program
    {
        static Stopwatch asyncStopwatch = new Stopwatch();
        static Stopwatch syncStopwatch = new Stopwatch();

        static readonly uint tps = 50;
        static readonly uint endTick = tps * 10;

        static void Main(string[] args)
        {
            AsyncExample();
            SyncExample();
            Console.ReadLine();
        }

        static void SyncExample()
        {
            //Creates a Tick Engine that fires 50 ticks per Second
            TickEngine.TickEngine engine = new TickEngine.TickEngine(tps);

            //Set Event
            engine.OnStart += (sender, e) =>
            {
                Console.WriteLine("[Sync] Started");
                syncStopwatch.Start();
            };
            engine.OnTick += (sender, e) =>
            {
                Console.WriteLine("[Sync] Tick: " + e.tick);
            };
            engine.OnPostTick += (sender, e) =>
            {
                if (e.tick == endTick)
                    engine.Stop();
            };
            engine.OnStop += (sender, e) =>
            {
                syncStopwatch.Stop();
                Console.WriteLine("[Sync] Stopped after " + syncStopwatch.Elapsed.TotalSeconds + " Seconds");
            };
            engine.Start();

            while (engine.IsEnabled)
            {
                engine.Pulse();
            }
        }

        static void AsyncExample()
        { 
            asyncStopwatch.Start();
            //Creates a Tick Engine that fires 50 ticks per Second
            TickEngine.TickEngine engine = new TickEngine.TickEngine(tps);

            engine.OnStart += (sender, e) =>
            {
                Console.WriteLine("[Async] Started");
                asyncStopwatch.Start();
            };

            engine.OnTick += (sender, e) =>
            {
                Console.WriteLine("[Async] Tick: " + e.tick);
            };

            engine.OnPostTick += (sender, e) =>
            {
                if (e.tick == endTick)
                    engine.Stop();
            };

            engine.OnStop += (sender, e) =>
            {
                asyncStopwatch.Stop();
                Console.WriteLine("[Async] Stopped after " + asyncStopwatch.Elapsed.TotalSeconds + " Seconds");
            };

            engine.StartWithPulse();
        }
    }
}
```
New contributor
AR234 is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
\$\endgroup\$
3
  • \$\begingroup\$ it's kinda similar to Stopwatch class to me, why didn't you use it instead? \$\endgroup\$
    – iSR5
    2 days ago
  • \$\begingroup\$ @iSR5 I updated my original with an edit, because I noticed I didn't do a great job explaining what this should do in the first place, key is that you can hook into the trigger on a set TPS \$\endgroup\$
    – AR234
    yesterday
  • 1
    \$\begingroup\$ Please do not edit the question, especially the code, after an answer has been posted. Changing the question may cause answer invalidation. Everyone needs to be able to see what the reviewer was referring to. What to do after the question has been answered. You can ask a follow up question with a link back to this question. \$\endgroup\$
    – pacmaninbw
    yesterday

1 Answer 1

3
\$\begingroup\$
        static readonly uint tps = 50;

Beautiful! No magic number trouble here. From the context I had it seemed clear enough, but consider throwing in a // ticks per second comment.

            //Creates a Tick Engine that fires 50 ticks per Second

Please delete this comment (and similarly in AsyncExample). We use code to explain the "what", and comments to explain the "why". My biggest objection to this line is that it requires next year's maintenance engineer to find / update a comment after updating the tps constant.


There's an interesting amount of copy-n-paste duplication between the sync and async examples, and in this case I feel it is OK. They are nearly unit tests, where we embrace copy-n-paste. And they have clear instructional value for someone who has a sync use case, or an async one, and wishes to get started.

If the two were in different source files, then it would be more convenient for the curious to quickly diff them.


        public event EventHandler<TickEventArgs> OnStart = null!;

This is accurate. And consistent. But maybe "arg" is a bit vague? Maybe each of these is a TickEventHandler ?

I really like the naming of the local variables, that's lovely. You already touched on how _tps is perhaps a bit awkward.


        static readonly uint endTick = tps * 10;
                _maxTick = TimeSpan.TicksPerSecond / _tps;

These kind of feel like they're doing the same thing, at the app level and library level. Except that they're not? Sorry, I find this code unclear. It seems like the units are clear, with the app level endTick shooting for ten seconds elapsed time. For the dimensional analysis of _maxTick to work out to "dimensionless" leaves me puzzled.


        /// <param name="resolution">How many Checks per Tickinterval, higher number more CPU usage, but higher accuracy</param>

I don't understand what "Tickinterval" means in this context.

The documentation seems to suggest that multiple check calls could happen per tick, which surprises me. I was thinking we might check every K ticks or so.

I'm fine with saying the code is "right". I'm just saying that the documentation left me with some puzzles to work out.

        /// Perform a pulse on the engine if enough time has passed a tick will be fired

Oohhhh! There's a concept that this is now explaining to me. Apparently there shall be K low-level pulses in between each tick. Hmm, what a curious approach. I am accustomed to consulting the clock to see time_till_next_tick and sleeping that long. But ok, I can see that this works.

I am reading Update.

It's pretty clear to me that I do not yet understand the concept that _maxTick embodies -- I do not find this is clear code that others could readily maintain. It definitely isn't dimensionless; it has units of "number of ticks".

I don't get the error analysis at all. Presumably there is some quantization trouble we're worried about, which we want to smooth out in the long run. Yet we're quantizing from microseconds (nanoseconds?) down to ticks, and keeping running sum of those. That doesn't make sense.

The usual approach would be to define a _nextScheduledTick in terms of wall clock time, and then frequent pulse() will eventually notice the clock is slightly after that scheduled time, triggering a tick and updating the schedule.

Up in Start I think this is the part I didn't initially notice:

            _timestamp = DateTime.Now.Ticks;

That is, it claims to have units of ~ seconds since 1970, yet clearly it has units of ticks (20 msec in this commit).

EDIT: no, apparently it's a unit of 100 nanoseconds.

Ok, now I understand why this code was so confusing.


I am reading the vendor documentation, and it turns out they have already given "tick" a well-known meaning of "100-nanosecond interval". I suppose one might use an identifier of tenth_usec for that.

There are three sections to any engine-based app you deploy:

  1. util (currently empty)
  2. library
  3. app

Within each section, the four characters "tick" should have a single, unambiguous meaning, or should be accompanied by a comment explaining its alternate meaning.

You have some decisions to make.

In my opinion the library and app sections should see only the kind of tick that you're describing here. And it should be in terms of a scheduling promise, rather than measuring an elapsed time since an epoch.

The (to be defined) utility layer should consume the vendor's Tick abstraction and convert it to SI units of seconds. Or microseconds or nanoseconds, something related to the natural world, and definitely not derived from computer clocks, timers, and their resolutions. The loss of clarity in communicating your technical ideas just isn't worth it.


                if (e.tick == endTick)
                    engine.Stop();

It's not obvious to me that real applications have a use case for terminating after N ticks have triggered. I recommend that you not expose such a running count via the API. If an application cares, it can certainly maintain its own counter. For this example I would rather see a termination criterion of "now > start + 10 seconds". Other apps might terminate when a demo vehicle crosses some finish line, or more likely according to a UI button being clicked.

Why not expose it? Because there's the temptation to equate current tick number with some number of seconds past an epoch. But that's not what it means, since it's valid to trigger fewer than tps ticks in a second if the system happens to be busy then. Stick to "scheduling" for your tick abstraction, and leave "time keeping" to well established interfaces that already offer such services.


FP timestamps get a bad rep because FP was slow on ancient machines, plus repeatedly doing

    stamp += .001

and then asking e.g. whether stamp == 1 won't work.

Consider adopting FP timestamps in your codebase. Why? Clarity. Plus you only really care about the comparison operator, now > scheduled, which will do just what you want. Fifty-three bits of mantissa is a lot, giving you considerable latitude to choose some sensible combination of resolution and starting epoch.

For example this standard library offers high-resolution "seconds since 1970" as a FP result.


Design consideration:

The examples do not sleep(), and it seems like they ought to, right? I mean, someone has to have the main event loop, and here it doesn't look like the library will, it looks like the higher level app will have it.

Then the app can pulse() quite often, and the library will tick() when appropriate and will never sleep.

The example while (engine.IsEnabled) { engine.Pulse(); } loop just doesn't make sense to me -- it doesn't call sleep. But even if it did, I would still want to see example code that sets up interrupt-based pulses to support a while (1) { sleep(1); } application. That is, if the app is continuously busy, it should still be easy for the library to produce pulses and hence ticks in the background. And yes, I get it that not all apps will want that, sometimes an app would rather not be interrupted, cool.


This code appears to run without crashing.

I would not be willing to delegate or accept maintenance tasks on this codebase in its current form. It appears that small changes to it would dramatically improve its clarity.

\$\endgroup\$
3
  • \$\begingroup\$ Thank you really much for your input, I mearly do programming as a hobby and having such a detailed breakdown is quite enlightening for me. I will rewrite the code with a lot of changes and one key feature should be that the target tps (which I will call the var from now on) should be at that speed but if more work needs to be done the actual ticks per second can go lower then the target. yeah the "resolution" part is one of the key things that bother me right now, this is why I will rewrite it and I think I got an Idea (because of your post) how to do it much better. \$\endgroup\$
    – AR234
    yesterday
  • \$\begingroup\$ I just realized that this is probably the problem with my explaination here, that yes in fact ticks have already a meaning, but there is this priciple in for example minecraft where there are a maximum of 20 ticks per second to calculate the world events and updates each tick. so it is not tied to the framerate. I use DateTime.ticks to enable much higher tickrates of 1000+ if this is really usefull I don't know but its there to have the capability to use it if nessacary \$\endgroup\$
    – AR234
    yesterday
  • \$\begingroup\$ oh and I didn't really get the FP timestamp part, tried googling it but not sure what you mean by it, since google didn't got me any results that matched your description \$\endgroup\$
    – AR234
    yesterday

Your Answer

AR234 is a new contributor. Be nice, and check out our Code of Conduct.

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.