UNPKG

@lf-lang/reactor-ts

Version:

A reactor-oriented programming framework in TypeScript

626 lines 22.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Alarm = exports.Origin = exports.Tag = exports.TimeValue = exports.TimeUnit = void 0; exports.getCurrentPhysicalTime = getCurrentPhysicalTime; /** * Time-related helper functions for reactors. * @author Marten Lohstroh (marten@berkeley.edu) * @author Matt Weber (matt.weber@berkeley.edu) */ const microtime_1 = __importDefault(require("microtime")); /** * Units (and conversion factors from nanoseconds) for time values. **/ var TimeUnit; (function (TimeUnit) { TimeUnit[TimeUnit["nsec"] = 1] = "nsec"; TimeUnit[TimeUnit["usec"] = 1000] = "usec"; TimeUnit[TimeUnit["msec"] = 1000000] = "msec"; TimeUnit[TimeUnit["sec"] = 1000000000] = "sec"; TimeUnit[TimeUnit["minute"] = 60000000000] = "minute"; TimeUnit[TimeUnit["hour"] = 3600000000000] = "hour"; TimeUnit[TimeUnit["day"] = 86400000000000] = "day"; TimeUnit[TimeUnit["week"] = 604800000000000] = "week"; })(TimeUnit || (exports.TimeUnit = TimeUnit = {})); /** * A time value given in nanosecond precision. To prevent overflow (which would * occur for time intervals spanning more than 0.29 years if a single JavaScript * number, which has 2^53 bits of precision, were to be used), we use _two_ * numbers to store a time value. The first number denotes the number of whole * seconds in the interval; the second number denotes the remaining number of * nanoseconds in the interval. This class serves as a base class for * `UnitBasedTimeValue`, which provides the convenience of defining time values * as a single number accompanied by a unit. * @see TimeUnit * @see UnitBasedTimeValue */ class TimeValue { seconds; nanoseconds; /** * Create a new time value. Both parameters must be non-negative integers; * an error will be thrown otherwise. The second parameter is optional. * @param seconds Number of seconds in the interval. * @param nanoseconds Remaining number of nanoseconds (defaults to zero). */ constructor(seconds, nanoseconds = 0) { this.seconds = seconds; this.nanoseconds = nanoseconds; if (!Number.isInteger(seconds) || !Number.isInteger(nanoseconds) || seconds < 0 || nanoseconds < 0) { if (seconds !== Number.MIN_SAFE_INTEGER) { throw new Error("Cannot instantiate a time interval based on negative or non-integer numbers."); } } } static zero() { return new TimeValue(0, 0); } static never() { return new TimeValue(Number.MIN_SAFE_INTEGER, 0); } static forever() { return new TimeValue(Number.MAX_SAFE_INTEGER, 0); } static secsAndNs(seconds, nanoSeconds) { return new TimeValue(seconds, nanoSeconds); } static secs(seconds) { return TimeValue.secsAndNs(seconds, 0); } static sec(seconds) { return TimeValue.secsAndNs(seconds, 0); } static msecs(microseconds) { return TimeValue.withUnits(microseconds, TimeUnit.msec); } static msec(microseconds) { return TimeValue.withUnits(microseconds, TimeUnit.msec); } static usecs(nanoseconds) { return TimeValue.withUnits(nanoseconds, TimeUnit.usec); } static usec(nanoseconds) { return TimeValue.withUnits(nanoseconds, TimeUnit.usec); } static nsecs(nanoseconds) { return TimeValue.withUnits(nanoseconds, TimeUnit.nsec); } static nsec(nanoseconds) { return TimeValue.withUnits(nanoseconds, TimeUnit.nsec); } // FIXME: Add more convenience methods /** * Return a new time value that denotes the duration of the time interval * encoded by this time value plus the time interval encoded by the time * value given as a parameter. * @param other The time value to add to this one. */ add(other) { if ((this.isNever() && other.isForever()) || (this.isForever() && other.isNever())) { // The sum of minus infinity and infinity is zero. return TimeValue.zero(); } else if (this.isNever() || other.isNever()) { // The sum of minus infinity and a normal tag is minus infinity. return TimeValue.never(); } else if (this.isForever() || other.isForever()) { // The sum of infinity and a normal tag is infinity. return TimeValue.forever(); } let seconds = this.seconds + other.seconds; let nanoseconds = this.nanoseconds + other.nanoseconds; if (nanoseconds >= TimeUnit.sec.valueOf()) { // Carry the second. seconds += 1; nanoseconds -= TimeUnit.sec; } // Check an overflow. if (!Number.isSafeInteger(seconds)) { // The overflow happens. return TimeValue.forever(); } else { return TimeValue.secsAndNs(seconds, nanoseconds); } } /** * Return a new time value that denotes the duration of the time interval * encoded by this time value minus the time interval encoded by the time * value given as a parameter. * @param other The time value to subtract from this one. */ subtract(other) { let s = this.seconds - other.seconds; let ns = this.nanoseconds - other.nanoseconds; if (ns < 0) { // Borrow a second s -= 1; ns += TimeUnit.sec; } if (s < 0) { throw new Error("Negative time value."); } return TimeValue.secsAndNs(s, ns); } multiply(factor) { let seconds = this.seconds * factor; let nanoseconds = this.nanoseconds * factor; if (nanoseconds >= TimeUnit.sec.valueOf()) { // Carry seconds. const carry = Math.floor(nanoseconds / TimeUnit.sec); seconds += carry; nanoseconds -= carry * TimeUnit.sec; } return TimeValue.secsAndNs(seconds, nanoseconds); } difference(other) { if (this.isEarlierThan(other)) { return other.subtract(this); } else { return this.subtract(other); } } /** * Return true if this time value denotes a time interval of equal length as * the interval encoded by the time value given as a parameter. * @param other The time value to compare to this one. */ isEqualTo(other) { return (this.seconds === other.seconds && this.nanoseconds === other.nanoseconds); } /** * Return true if this denotes a time interval of length zero. */ isZero() { if (this.seconds === 0 && this.nanoseconds === 0) { return true; } else { return false; } } isNever() { if (this.seconds === Number.MIN_SAFE_INTEGER && this.nanoseconds === 0) { return true; } else { return false; } } isForever() { if (this.seconds === Number.MAX_SAFE_INTEGER) { return true; } else { return false; } } /** * Return true if this time value denotes a time interval of smaller length * than the time interval encoded by the time value given as a parameter; * return false otherwise. * * NOTE: Performing this comparison involves a conversion to a big integer * and is therefore relatively costly. * * @param other The time value to compare to this one. * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt|BigInt} for further information. */ isEarlierThan(other) { if (this.seconds < other.seconds) { return true; } if (this.seconds === other.seconds && this.nanoseconds < other.nanoseconds) { return true; } return false; } /** * Return true if this time value is later than the time given as a parameter. * * @param other The time value to compare to this one. */ isLaterThan(other) { if (this.seconds > other.seconds) { return true; } if (this.seconds === other.seconds && this.nanoseconds > other.nanoseconds) { return true; } return false; } /** * Return a millisecond representation of this time value. */ toMilliseconds() { return this.seconds * 1000 + Math.ceil(this.nanoseconds / 1000000); } /** * Print the number of seconds and nanoseconds in the time interval encoded * by this time value. */ toString() { return `(${this.seconds} secs; ${this.nanoseconds} nsecs)`; } /** * Return a tuple that holds the seconds and remaining nanoseconds that * jointly represent this time value. */ toTimeTuple() { return [this.seconds, this.nanoseconds]; } /** * Get a 64 bit binary, little endian representation of this TimeValue. * Used by federates. */ toBinary() { const buff = Buffer.alloc(8); if (this.seconds === Number.MIN_SAFE_INTEGER) { buff.writeBigUInt64LE(BigInt(0x8000000000000000n), 0); } else if (this.seconds === Number.MAX_SAFE_INTEGER) { buff.writeBigUInt64LE(BigInt(0x7fffffffffffffffn), 0); } else { const billion = BigInt(TimeUnit.sec); const bigTime = BigInt(this.nanoseconds) + BigInt(this.seconds) * billion; // Ensure the TimeValue fits into a 64 unsigned integer. const clampedTime = BigInt.asUintN(64, bigTime); if (clampedTime !== bigTime) { throw new Error(`TimeValue ${this.toString()} is too big to fit into ` + "a 64 bit unsigned integer"); } buff.writeBigUInt64LE(bigTime, 0); } return buff; } /** * Create a TimeValue from a 64bit little endian unsigned integer in a buffer. * @param buffer A 64 bit unsigned integer. Little endian. */ static fromBinary(buffer) { const billion = BigInt(TimeUnit.sec); // To avoid overflow and floating point errors, work with BigInts. const bigTime = buffer.readBigUInt64LE(0); if (bigTime === BigInt(0x8000000000000000n)) { return TimeValue.never(); } else if (bigTime === BigInt(0x7fffffffffffffffn)) { return TimeValue.forever(); } const bigSeconds = bigTime / billion; const bigNSeconds = bigTime % billion; return TimeValue.secsAndNs(Number(bigSeconds), Number(bigNSeconds)); } /** * Give a value and time unit, return a new time value. * * The value is a `number` that is required to be * a positive integer. The time unit must be a member of the `TimeUnit` enum. * @param value A number (which must be a positive integer) that denotes * the length of the specified time interval, expressed as a multiple of * the given time unit. * @param unit The unit of measurement that applies to the given value. * @returns */ static withUnits(value, unit) { if (!Number.isInteger(value)) { throw new Error("Non-integer time values are illegal."); } if (value < 0) { throw new Error("Negative time values are illegal."); } const billion = BigInt(TimeUnit.sec); // To avoid overflow and floating point errors, work with BigInts. const bigT = BigInt(value) * BigInt(unit); const bigSeconds = bigT / billion; if (bigSeconds > Number.MAX_SAFE_INTEGER) { throw new Error("Unable to instantiate time value: value too large."); } return TimeValue.secsAndNs(Number(bigSeconds), Number(bigT % billion)); } } exports.TimeValue = TimeValue; /** * A superdense time instant, represented as a time value `time` (i.e., * time elapsed since Epoch) paired with a microstep index to keep track of * iterations in superdense time. For each such iteration, `time` remains * the same, but `microstep` is incremented. */ class Tag { microstep; /** * Time elapsed since Epoch. */ time; /** * Create a new tag using a time value and a microstep. * @param timeSinceEpoch Time elapsed since Epoch. * @param microstep Superdense time index. */ constructor(timeSinceEpoch, microstep = 0) { this.microstep = microstep; if (!Number.isInteger(microstep)) { throw new Error("Microstep must be integer."); } if (microstep < 0) { throw new Error("Microstep must be positive."); } this.time = timeSinceEpoch; } /** * Return `true` if this time instant is earlier than the tag given as a * parameter, false otherwise. For two tags with an equal `time`, one * instant is earlier than the other if its `microstep` is less than the * `microstep` of the other. * @param other The time instant to compare against this one. */ isSmallerThan(other) { return (this.time.isEarlierThan(other.time) || (this.time.isEqualTo(other.time) && this.microstep < other.microstep)); } /** * Return `true` if the tag is smaller than or equal to the tag given as a parameter. * @param other The time instant to compare against this one. */ isSmallerThanOrEqualTo(other) { return !this.isGreaterThan(other); } /** * Return `true` if the tag is greater than the tag given as a parameter. * @param other The time instant to compare against this one. */ isGreaterThan(other) { return (this.time.isLaterThan(other.time) || (this.time.isEqualTo(other.time) && this.microstep > other.microstep)); } /** * Return `true` if the tag is greater than or equal to the tag given as a parameter. * @param other The time instant to compare against this one. */ isGreaterThanOrEqualTo(other) { return !this.isSmallerThan(other); } /** * Return `true` if this tag is simultaneous with the tag given as * a parameter, false otherwise. Both `time` and `microstep` must be equal * for two tags to be simultaneous. * @param other The time instant to compare against this one. */ isSimultaneousWith(other) { return (this.time.isEqualTo(other.time) && this.microstep === other.microstep); } /** * Get a new time instant that represents this time instant plus * the given delay. The `microstep` of this time instant is ignored; * the returned time instant has a `microstep` of zero if the delay * is greater than zero. If the delay equals zero or is undefined, * the tag is returned unchanged with its current `microstep`. * @param delay The time interval to add to this time instant. */ getLaterTag(delay) { if (delay === undefined) { return this; } else if (delay.isZero()) { return new Tag(this.time, this.microstep + 1); } else { return new Tag(delay.add(this.time), 0); } } /** * Get a new time instant that has the same `time` but n `microsteps` later. */ getMicroStepsLater(n) { return new Tag(this.time, this.microstep + n); } /** * Obtain a time interval that represents the absolute (i.e., positive) time * difference between this time interval and the tag given as a parameter. * @param other The time instant for which to compute the absolute * difference with this time instant. */ getTimeDifference(other) { if (this.isSmallerThan(other)) { return other.time.subtract(this.time); } else { return this.time.subtract(other.time); } } /** * Return a human-readable string presentation of this time instant. */ toString() { return `(${this.time.toString()}, ${this.microstep})`; } /** * Get a 12-byte buffer for the tag. * Used by federates. */ toBinary() { const timeBuffer = this.time.toBinary(); const buf = Buffer.alloc(12); timeBuffer.copy(buf, 0, 0); buf.writeInt32LE(this.microstep, 8); return buf; } /** * Create a Tag from 8-byte TimeValue and 4-byte microstep in a buffer. * @param buffer A buffer of TimeValue and microstep. */ static fromBinary(buffer) { const timeBuffer = Buffer.alloc(8); buffer.copy(timeBuffer, 0, 0, 8); const time = TimeValue.fromBinary(timeBuffer); const microstep = buffer.readUInt32LE(8); return new Tag(time, microstep); } } exports.Tag = Tag; /** * A descriptor to be used when scheduling events to denote as to whether * an event should occur with a delay relative to _physical time_ or _logical * time_. */ var Origin; (function (Origin) { Origin["physical"] = "physical"; Origin["logical"] = "logical"; })(Origin || (exports.Origin = Origin = {})); /** * Return a time value that reflects the current physical time as reported * by the platform. */ function getCurrentPhysicalTime() { const t = microtime_1.default.now(); const seconds = Math.floor(t / 1000000); const nseconds = t * 1000 - seconds * TimeUnit.sec; return TimeValue.secsAndNs(seconds, nseconds); } /** * Simple but accurate alarm that makes use of high-resolution timer. * The algorithm is inspired by nanotimer, written by Kevin Briggs. * @author Marten Lohstroh (marten@berkeley.edu) */ class Alarm { /** * Handle for regular timeout, used for delays > 25 ms. */ deferredRef; /** * Handle for immediate, used for polling once the remaining delay is < 25 * ms. */ immediateRef; /** * Delay in terms of milliseconds, used when deferring to regular timeout. */ loResDelay = 0; /** * Start of the delay interval; tuple of seconds and nanoseconds. */ hiResStart = [0, 0]; /** * The delay interval in high resolution; tuple of seconds and nanoseconds. */ hiResDelay = [0, 0]; /** * Indicates whether the alarm has been set or not. */ active = false; /** * Disable any scheduled timeouts or immediate events, and set the timer to * inactive. */ unset() { if (this.deferredRef != null) { clearTimeout(this.deferredRef); this.deferredRef = undefined; } if (this.immediateRef != null) { clearImmediate(this.immediateRef); this.immediateRef = undefined; } this.active = false; } /** * Once the alarm has been initialized, see if the task can be performed or * a longer wait is necessary. * @param task The task to perform. * @param callback Optional callback used to report the wait time. */ try(task, callback) { // Record the current time. const hiResDif = process.hrtime(this.hiResStart); // See whether the requested delay has elapsed. if (this.hiResDelay[0] < hiResDif[0] || (this.hiResDelay[0] === hiResDif[0] && this.hiResDelay[1] < hiResDif[1])) { // No more immediates a scheduled. this.immediateRef = undefined; // The delay has elapsed; perform the task. if (this.active) { // If this attempt is the result of a deferred request, perform // the task synchronously. this.active = false; task(); if (callback != null) { callback(TimeValue.secsAndNs(...hiResDif)); } } else { // If this attempt is the result of a direct call to `set`, push // task onto the nextTick queue. This unwinds the stack, but it // bypasses the regular event queue. If events are recursively // requested to be performed with zero or near zero delay, this // will not cause the call stack to exceed its maximum size, but // it will starve I/O. this.active = true; process.nextTick(() => { if (this.active) { this.active = false; task(); if (callback != null) { callback(TimeValue.secsAndNs(...hiResDif)); } } }); } } else { // The delay has not yet elapsed. // The following logic is based on the implementation of nanotimer. if (this.loResDelay > 25) { if (!this.active) { this.deferredRef = setTimeout(() => { this.try(task, callback); }, this.loResDelay - 25); } else { this.deferredRef = undefined; this.immediateRef = setImmediate(() => { this.try(task, callback); }); } } else { this.immediateRef = setImmediate(() => { this.try(task, callback); }); } this.active = true; } } /** * Set the alarm. * @param task The task to be performed. * @param delay The time has to elapse before the task can be performed. * @param callback Optional callback used to report the wait time. */ set(task, delay, callback) { // Reset the alarm if it was already active. if (this.active) { this.unset(); } // Compute the delay this.hiResDelay = delay.toTimeTuple(); this.loResDelay = delay.toMilliseconds(); // Record the beginning of the delay interval. this.hiResStart = process.hrtime(); this.try(task, callback); } } exports.Alarm = Alarm; //# sourceMappingURL=time.js.map