@actyx/sdk
Version:
Actyx SDK
284 lines • 8.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.runStats = exports.RunStats = exports.GaugeMap = exports.DurationMap = exports.CounterMap = void 0;
/*
* Actyx SDK: Functions for writing distributed apps
* deployed on peer-to-peer networks, without any servers.
*
* Copyright (C) 2021 Actyx AG
*/
const Option_1 = require("fp-ts/lib/Option");
const t = require("io-ts");
const rxjs_1 = require("../../node_modules/rxjs");
// Copypasta to avoid weird circular dependency issues
const lookup = (m, k) => m[k];
const Timestamp = {
now: () => Date.now() * 1000,
};
const CounterMapEntry = t.type({
count: t.number,
last: t.number,
});
const CounterMapInternal = t.record(t.string, CounterMapEntry);
const CounterMapMut = t.record(t.string, t.tuple([t.number, t.number]));
exports.CounterMap = t.readonly(CounterMapMut);
const DurationStats = t.union([
t.readonly(t.type({
count: t.number,
min: t.number,
max: t.number,
median: t.number,
_90: t.number,
_95: t.number,
_99: t.number,
pending: t.number,
discarded: t.number,
})),
t.readonly(t.type({
pending: t.number,
discarded: t.number,
})),
]);
const DurationMapMut = t.record(t.string, DurationStats);
exports.DurationMap = t.readonly(DurationMapMut);
const addDurationStats = (stats, duration) => {
const { durations } = stats;
// only sample the first 1000 in each time interval
if (durations.length < 1000) {
durations.push(duration);
}
};
const addPendingOperation = (stats, at) => {
const count = stats.pending[at] || 0;
stats.pending[at] = count + 1;
};
const endPendingOperation = (stats, from, to) => {
const count = stats.pending[from] || undefined;
if (count === undefined || count <= 0) {
stats.discarded += 1;
return;
}
const newCount = count - 1;
if (newCount === 0) {
delete stats.pending[from];
}
else {
stats.pending[from] = newCount;
}
addDurationStats(stats, to - from);
};
const createEntry = (duration) => ({
durations: [duration],
pending: {},
discarded: 0,
});
const createEntryAt = (at) => ({
durations: [],
pending: { [at]: 1 },
discarded: 0,
});
const createDiscardedEntry = () => ({
durations: [],
pending: {},
discarded: 1,
});
const getDurationStats = (stats) => {
const count = stats.durations.length;
const pending = Object.values(stats.pending).reduce((acc, v) => acc + v, 0);
const discarded = stats.discarded;
if (count === 0) {
if (pending !== 0 || discarded !== 0)
return (0, Option_1.some)({ pending, discarded });
else
return Option_1.none;
}
const d = stats.durations.sort((a, b) => a - b);
const min = d[0];
const median = d[Math.floor(count / 2)];
const _90 = d[Math.floor(count * 0.9)];
const _95 = d[Math.floor(count * 0.95)];
const _99 = d[Math.floor(count * 0.99)];
const max = d[count - 1];
// clear the stats, this is reported per interval
stats.durations = [];
stats.discarded = 0;
return (0, Option_1.some)({
count,
pending,
discarded,
min,
median,
_90,
_95,
_99,
max,
});
};
const mkCounters = () => {
const counters = {};
const add = (name, count) => {
const entry = lookup(counters, name);
if (entry === undefined) {
counters[name] = { count: count || 1, last: 0 };
}
else {
entry.count += count || 1;
}
};
const current = () => {
const result = {};
Object.entries(counters).forEach(([name, entry]) => {
result[name] = [entry.count, entry.count - entry.last];
entry.last = entry.count;
});
return result;
};
return {
add,
current,
};
};
const createDurationStats = () => {
const durations = {};
const add = (name, duration) => {
const entry = lookup(durations, name);
if (entry === undefined) {
durations[name] = createEntry(duration);
}
else {
addDurationStats(entry, duration);
}
};
const start = (name, at) => {
const entry = lookup(durations, name);
if (entry === undefined) {
durations[name] = createEntryAt(at);
}
else {
addPendingOperation(entry, at);
}
};
const end = (name, from, to) => {
const entry = lookup(durations, name);
if (entry === undefined) {
durations[name] = createDiscardedEntry();
return;
}
else {
endPendingOperation(entry, from, to);
}
};
const getAndClear = () => {
const result = {};
Object.entries(durations).forEach(([key, value]) => {
(0, Option_1.map)((stats) => {
result[key] = stats;
})(getDurationStats(value));
if (Object.keys(value.pending).length === 0) {
// If nothing is pending then this object is now useless, remove it.
// This metric may be repopulated in the next interval.
delete durations[key];
}
});
return result;
};
return {
add,
getAndClear,
start,
end,
};
};
const profileSync = (durations) => (name) => (block) => {
const t0 = Timestamp.now();
let result;
try {
result = block();
durations.add(name, Timestamp.now() - t0);
return result;
}
catch (e) {
durations.add(name, Timestamp.now() - t0);
throw e;
}
};
const profileObservable = (durations, counters) => (name, n) => (inner) => {
const maxCount = n || 1;
return new rxjs_1.Observable((subscriber) => {
let t0 = Timestamp.now();
let count = 0;
durations.start(name, t0);
return inner.subscribe({
next: (value) => {
count++;
if (count <= maxCount) {
const t1 = Timestamp.now();
durations.end(name, t0, t1);
t0 = t1;
}
if (count < maxCount) {
durations.start(name, t0);
}
subscriber.next(value);
},
error: (reason) => {
counters.add(`errprof-${name}`);
count++;
if (count <= maxCount) {
durations.end(name, t0, Timestamp.now());
}
subscriber.error(reason);
},
complete: () => {
count++;
if (count <= maxCount) {
durations.end(name, t0, Timestamp.now());
}
subscriber.complete();
},
});
});
};
const GaugeMapMut = t.record(t.string, t.type({
last: t.number,
max: t.number,
}));
exports.GaugeMap = t.readonly(GaugeMapMut);
const mkGauges = () => {
const gauges = {};
const set = (name, value) => {
const old = gauges[name] || { last: value, max: value };
const next = { last: value, max: Math.max(old.max, value) };
gauges[name] = next;
};
const current = () => ({ ...gauges });
return {
set,
current,
};
};
const createRunStats = () => {
const durations = createDurationStats();
const counters = mkCounters();
const profile = {
profileSync: profileSync(durations),
profileObservable: profileObservable(durations, counters),
};
const gauges = mkGauges();
return {
counters,
durations,
profile,
gauges,
};
};
exports.RunStats = {
create: createRunStats,
};
/**
* Global statistics singleton.
*
* In the same file to avoid circular dependency issues.
*/
exports.runStats = exports.RunStats.create();
//# sourceMappingURL=runStats.js.map