UNPKG

veffect

Version:

powerful TypeScript validation library built on the robust foundation of Effect combining exceptional type safety, high performance, and developer experience. Taking inspiration from Effect's functional principles, VEffect delivers a balanced approach tha

385 lines (384 loc) 12.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.summary = exports.onUpdate = exports.make = exports.histogram = exports.gauge = exports.frequency = exports.counter = exports.MetricHookTypeId = void 0; var Duration = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("../../Duration.js")); var _Function = /*#__PURE__*/require("../../Function.js"); var number = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("../../Number.js")); var Option = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("../../Option.js")); var _Pipeable = /*#__PURE__*/require("../../Pipeable.js"); var ReadonlyArray = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("../../ReadonlyArray.js")); var metricState = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("./state.js")); function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } /** @internal */ const MetricHookSymbolKey = "effect/MetricHook"; /** @internal */ const MetricHookTypeId = exports.MetricHookTypeId = /*#__PURE__*/Symbol.for(MetricHookSymbolKey); const metricHookVariance = { /* c8 ignore next */ _In: _ => _, /* c8 ignore next */ _Out: _ => _ }; /** @internal */ const make = options => ({ [MetricHookTypeId]: metricHookVariance, pipe() { return (0, _Pipeable.pipeArguments)(this, arguments); }, ...options }); /** @internal */ exports.make = make; const onUpdate = exports.onUpdate = /*#__PURE__*/(0, _Function.dual)(2, (self, f) => ({ [MetricHookTypeId]: metricHookVariance, pipe() { return (0, _Pipeable.pipeArguments)(this, arguments); }, get: self.get, update: input => { self.update(input); return f(input); } })); const bigint0 = /*#__PURE__*/BigInt(0); /** @internal */ const counter = key => { let sum = key.keyType.bigint ? bigint0 : 0; const canUpdate = key.keyType.incremental ? key.keyType.bigint ? value => value >= bigint0 : value => value >= 0 : _value => true; return make({ get: () => metricState.counter(sum), update: value => { if (canUpdate(value)) { sum = sum + value; } } }); }; /** @internal */ exports.counter = counter; const frequency = _key => { const values = new Map(); const update = word => { const slotCount = values.get(word) ?? 0; values.set(word, slotCount + 1); }; return make({ get: () => metricState.frequency(values), update }); }; /** @internal */ exports.frequency = frequency; const gauge = (_key, startAt) => { let value = startAt; return make({ get: () => metricState.gauge(value), update: v => { value = v; } }); }; /** @internal */ exports.gauge = gauge; const histogram = key => { const bounds = key.keyType.boundaries.values; const size = bounds.length; const values = new Uint32Array(size + 1); const boundaries = new Float32Array(size); let count = 0; let sum = 0; let min = Number.MAX_VALUE; let max = Number.MIN_VALUE; (0, _Function.pipe)(bounds, ReadonlyArray.sort(number.Order), ReadonlyArray.map((n, i) => { boundaries[i] = n; })); // Insert the value into the right bucket with a binary search const update = value => { let from = 0; let to = size; while (from !== to) { const mid = Math.floor(from + (to - from) / 2); const boundary = boundaries[mid]; if (value <= boundary) { to = mid; } else { from = mid; } // The special case when to / from have a distance of one if (to === from + 1) { if (value <= boundaries[from]) { to = from; } else { from = to; } } } values[from] = values[from] + 1; count = count + 1; sum = sum + value; if (value < min) { min = value; } if (value > max) { max = value; } }; const getBuckets = () => { const builder = Array(size); let cumulated = 0; for (let i = 0; i < size; i++) { const boundary = boundaries[i]; const value = values[i]; cumulated = cumulated + value; builder[i] = [boundary, cumulated]; } return builder; }; return make({ get: () => metricState.histogram({ buckets: getBuckets(), count, min, max, sum }), update }); }; /** @internal */ exports.histogram = histogram; const summary = key => { const { error, maxAge, maxSize, quantiles } = key.keyType; const sortedQuantiles = (0, _Function.pipe)(quantiles, ReadonlyArray.sort(number.Order)); const values = Array(maxSize); let head = 0; let count = 0; let sum = 0; let min = Number.MAX_VALUE; let max = Number.MIN_VALUE; // Just before the snapshot we filter out all values older than maxAge const snapshot = now => { const builder = []; // If the buffer is not full yet it contains valid items at the 0..last // indices and null values at the rest of the positions. // // If the buffer is already full then all elements contains a valid // measurement with timestamp. // // At any given point in time we can enumerate all the non-null elements in // the buffer and filter them by timestamp to get a valid view of a time // window. // // The order does not matter because it gets sorted before passing to // `calculateQuantiles`. let i = 0; while (i !== maxSize - 1) { const item = values[i]; if (item != null) { const [t, v] = item; const age = Duration.millis(now - t); if (Duration.greaterThanOrEqualTo(age, Duration.zero) && age <= maxAge) { builder.push(v); } } i = i + 1; } return calculateQuantiles(error, sortedQuantiles, ReadonlyArray.sort(builder, number.Order)); }; const observe = (value, timestamp) => { if (maxSize > 0) { head = head + 1; const target = head % maxSize; values[target] = [timestamp, value]; } count = count + 1; sum = sum + value; if (value < min) { min = value; } if (value > max) { max = value; } }; return make({ get: () => metricState.summary({ error, quantiles: snapshot(Date.now()), count, min, max, sum }), update: ([value, timestamp]) => observe(value, timestamp) }); }; /** @internal */ exports.summary = summary; const calculateQuantiles = (error, sortedQuantiles, sortedSamples) => { // The number of samples examined const sampleCount = sortedSamples.length; if (!ReadonlyArray.isNonEmptyReadonlyArray(sortedQuantiles)) { return ReadonlyArray.empty(); } const head = sortedQuantiles[0]; const tail = sortedQuantiles.slice(1); const resolvedHead = resolveQuantile(error, sampleCount, Option.none(), 0, head, sortedSamples); const resolved = ReadonlyArray.of(resolvedHead); tail.forEach(quantile => { resolved.push(resolveQuantile(error, sampleCount, resolvedHead.value, resolvedHead.consumed, quantile, resolvedHead.rest)); }); return ReadonlyArray.map(resolved, rq => [rq.quantile, rq.value]); }; /** @internal */ const resolveQuantile = (error, sampleCount, current, consumed, quantile, rest) => { let error_1 = error; let sampleCount_1 = sampleCount; let current_1 = current; let consumed_1 = consumed; let quantile_1 = quantile; let rest_1 = rest; let error_2 = error; let sampleCount_2 = sampleCount; let current_2 = current; let consumed_2 = consumed; let quantile_2 = quantile; let rest_2 = rest; // eslint-disable-next-line no-constant-condition while (1) { // If the remaining list of samples is empty, there is nothing more to resolve if (!ReadonlyArray.isNonEmptyReadonlyArray(rest_1)) { return { quantile: quantile_1, value: Option.none(), consumed: consumed_1, rest: [] }; } // If the quantile is the 100% quantile, we can take the maximum of all the // remaining values as the result if (quantile_1 === 1) { return { quantile: quantile_1, value: Option.some(ReadonlyArray.lastNonEmpty(rest_1)), consumed: consumed_1 + rest_1.length, rest: [] }; } // Split into two chunks - the first chunk contains all elements of the same // value as the chunk head const sameHead = ReadonlyArray.span(rest_1, n => n <= rest_1[0]); // How many elements do we want to accept for this quantile const desired = quantile_1 * sampleCount_1; // The error margin const allowedError = error_1 / 2 * desired; // Taking into account the elements consumed from the samples so far and the // number of same elements at the beginning of the chunk, calculate the number // of elements we would have if we selected the current head as result const candConsumed = consumed_1 + sameHead[0].length; const candError = Math.abs(candConsumed - desired); // If we haven't got enough elements yet, recurse if (candConsumed < desired - allowedError) { error_2 = error_1; sampleCount_2 = sampleCount_1; current_2 = ReadonlyArray.head(rest_1); consumed_2 = candConsumed; quantile_2 = quantile_1; rest_2 = sameHead[1]; error_1 = error_2; sampleCount_1 = sampleCount_2; current_1 = current_2; consumed_1 = consumed_2; quantile_1 = quantile_2; rest_1 = rest_2; continue; } // If we have too many elements, select the previous value and hand back the // the rest as leftover if (candConsumed > desired + allowedError) { return { quantile: quantile_1, value: current_1, consumed: consumed_1, rest: rest_1 }; } // If we are in the target interval, select the current head and hand back the leftover after dropping all elements // from the sample chunk that are equal to the current head switch (current_1._tag) { case "None": { error_2 = error_1; sampleCount_2 = sampleCount_1; current_2 = ReadonlyArray.head(rest_1); consumed_2 = candConsumed; quantile_2 = quantile_1; rest_2 = sameHead[1]; error_1 = error_2; sampleCount_1 = sampleCount_2; current_1 = current_2; consumed_1 = consumed_2; quantile_1 = quantile_2; rest_1 = rest_2; continue; } case "Some": { const prevError = Math.abs(desired - current_1.value); if (candError < prevError) { error_2 = error_1; sampleCount_2 = sampleCount_1; current_2 = ReadonlyArray.head(rest_1); consumed_2 = candConsumed; quantile_2 = quantile_1; rest_2 = sameHead[1]; error_1 = error_2; sampleCount_1 = sampleCount_2; current_1 = current_2; consumed_1 = consumed_2; quantile_1 = quantile_2; rest_1 = rest_2; continue; } return { quantile: quantile_1, value: Option.some(current_1.value), consumed: consumed_1, rest: rest_1 }; } } } throw new Error("BUG: MetricHook.resolveQuantiles - please report an issue at https://github.com/Effect-TS/effect/issues"); }; //# sourceMappingURL=hook.js.map