js-awe
Version:
Awesome js utils including - plan: An Asynchronous control flow with a functional taste - Chrono: record and visualize timelines in the console
392 lines (391 loc) • 18.3 kB
JavaScript
import { arraySorter, pushUniqueKeyOrChange, sorterByPaths, pushUniqueKey, CustomError, pushAt } from './jsUtils.js';
import { groupByWithCalc, R } from './ramdaExt.js';
import { Table, consoleTable } from './table/table.js';
import { Text } from './table/components/text.js';
import { Timeline } from './table/components/timeline.js';
import { performance } from 'perf_hooks';
let myGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : typeof this !== 'undefined' ? this : {};
// needed only for debuging
//import { RE } from './ramdaExt.js';
function Chrono() {
let milisecondsNow;
if (performance?.now)
milisecondsNow = () => performance.now();
if (milisecondsNow === undefined)
milisecondsNow = () => Date.now();
let historyTimeIntervals = {};
let chronoEvents = {};
createTimeEvent('chronoCreation');
let rangeType = Range({
type: 'miliseconds',
displayFormat: 'ms',
referenceMiliseconds: chronoEvents['chronoCreation'].miliseconds
});
function createTimeEvent(eventName) {
chronoEvents[eventName] = {
date: new Date(),
miliseconds: milisecondsNow()
};
}
function validateEventName(eventName) {
if (typeof eventName !== 'string' || isNaN(Number(eventName)) === false)
throw new CustomError('EVENT_NAME_MUST_HAVE_ALPHABETICS_CHARS', `Event name '${eventName}' must be of type string and contain some non numeric character`, eventName);
}
function time(eventNames) {
let currentMiliseconds = milisecondsNow();
let listOfEvents = typeof eventNames === 'string'
? [eventNames]
: eventNames;
listOfEvents.forEach(eventName => {
validateEventName(eventName);
historyTimeIntervals[eventName] ??= {};
historyTimeIntervals[eventName].start ??= [];
historyTimeIntervals[eventName].start.push(currentMiliseconds);
});
}
function timeEnd(eventNames) {
let currentMiliseconds = milisecondsNow();
let listOfEvents = typeof eventNames === 'string'
? [eventNames]
: eventNames;
listOfEvents.forEach(eventName => {
if (historyTimeIntervals[eventName] === undefined) {
throw new CustomError('EVENT_NAME_NOT_FOUND', `No such Label '${eventName}' for .timeEnd(...)`, eventName);
}
let start = historyTimeIntervals[eventName].start.pop();
if (start === undefined) {
throw new CustomError('EVENT_NAME_ALREADY_CONSUMED', `eventName: '${eventName}' was already consumed by a previous call to .timeEnd(...)`, eventName);
}
historyTimeIntervals[eventName].ranges ??= [];
historyTimeIntervals[eventName].ranges.push(rangeType(start, currentMiliseconds));
});
}
function fillWithUndefinedRanges() {
Object.entries(historyTimeIntervals).forEach(([eventName, currentEventValues], indexEvent, intervalEntries) => {
let indexRangeForEvent = 0;
intervalEntries[0][1].ranges.forEach(({ start: startRef, end: endRef }, indexRangeRef) => {
if (indexEvent === 0) {
currentEventValues.ranges[indexRangeRef] = rangeType(startRef, endRef, indexRangeRef);
return;
}
const isCurrentEventSameIntervalAsReference = () => {
const currentEventStart = currentEventValues.ranges[indexRangeForEvent]?.start;
const nextEventStart = intervalEntries[0][1].ranges[indexRangeRef + 1]?.start;
const isStartOfCurrentEventAfterStartOfReference = currentEventStart >= startRef;
const isStartOfCurrentEventBeforeStartOfNextReference = indexRangeRef + 1 === intervalEntries[0][1].ranges.length
|| currentEventStart < nextEventStart;
return isStartOfCurrentEventAfterStartOfReference
&& isStartOfCurrentEventBeforeStartOfNextReference;
};
let foundMatchingInterval = false;
while (isCurrentEventSameIntervalAsReference()) {
foundMatchingInterval = true;
const currentRange = currentEventValues.ranges[indexRangeForEvent];
const nextRange = currentEventValues.ranges[indexRangeForEvent + 1];
const previousRange = currentEventValues.ranges[indexRangeForEvent - 1];
// Accrued ranges for same interval, deleting the current one
const isSameIntervalAsPreviousOne = previousRange?.interval === indexRangeRef;
if (isSameIntervalAsPreviousOne) {
currentEventValues.ranges[indexRangeRef] =
rangeType(currentRange.start - (previousRange.end - previousRange.start), currentRange.end, indexRangeRef);
currentEventValues.ranges.splice(indexRangeForEvent, 1);
}
else {
currentEventValues.ranges[indexRangeForEvent] =
rangeType(currentRange.start, currentRange.end, indexRangeRef);
indexRangeForEvent++;
}
}
if (foundMatchingInterval === false) {
pushAt(indexRangeForEvent, rangeType(undefined, undefined, indexRangeRef), currentEventValues.ranges);
indexRangeForEvent++;
}
});
});
}
function findParentRanges(eventValues, indexEvent, intervalEntries) {
let isNotAParent = true;
while (indexEvent !== 0 && isNotAParent === true) {
indexEvent--;
isNotAParent = intervalEntries[indexEvent][1].ranges.some(({ start, end }, index) => (start === undefined || end === undefined) &&
(eventValues.ranges[index].start !== undefined || eventValues.ranges[index].end !== undefined));
}
return [intervalEntries[indexEvent][1].ranges, intervalEntries[indexEvent][0]];
}
//TDL
function average() {
fillWithUndefinedRanges();
historyTimeIntervals = Object.entries(historyTimeIntervals).reduce((newHistoryIntervals, [eventName, eventValues], indexEvent, intervalEntries) => {
const [parentRanges, parentEventName] = findParentRanges(eventValues, indexEvent, intervalEntries);
const [totalElapse, totalEndToStartGap, totalStartToStartGap] = eventValues.ranges.reduce(([totalElapse, totalEndToStartGap, totalStartToStartGap], { start = 0, end = 0 }, indexRange) => {
totalElapse = totalElapse + end - start;
if (indexEvent !== 0 && start !== 0 && end !== 0) {
totalEndToStartGap = totalEndToStartGap + start - parentRanges[indexRange].end;
totalStartToStartGap = totalStartToStartGap + start - parentRanges[indexRange].start;
}
return [
totalElapse,
totalEndToStartGap,
totalStartToStartGap
];
}, [0, 0, 0]);
let averagetart;
let avarageEventEnd;
const totalRangesWithValues = eventValues.ranges.filter(({ start, end }) => start !== undefined & end !== undefined).length;
if (indexEvent === 0) {
averagetart = intervalEntries[0][1].ranges[0].start;
}
if (indexEvent !== 0 && Math.abs(totalEndToStartGap) <= Math.abs(totalStartToStartGap)) {
averagetart =
newHistoryIntervals[parentEventName].ranges[0].end +
totalEndToStartGap / totalRangesWithValues;
}
if (indexEvent !== 0 && Math.abs(totalStartToStartGap) < Math.abs(totalEndToStartGap)) {
averagetart =
newHistoryIntervals[parentEventName].ranges[0].start +
totalStartToStartGap / eventValues.ranges.length;
}
avarageEventEnd = averagetart + totalElapse / eventValues.ranges.length;
newHistoryIntervals[eventName] =
{
ranges: [
rangeType(averagetart, avarageEventEnd, 0)
]
};
return newHistoryIntervals;
}, {});
//range: { start:3.5852760076522827 <-133.67405599355698-> end:137.25933200120926 }
}
function eventsReport(events) {
const entriesEvents = Object.entries(events);
const [minMilisecondss, maxMilisecondss] = entriesEvents.reduce((acum, [eventName, eventObject]) => {
eventObject.ranges.forEach(range => {
if (acum[0] > range.start)
acum[0] = range.start;
if (acum[1] < range.end)
acum[1] = range.end;
});
return acum;
}, [Infinity, 0]);
return events;
}
function totalEventsElapseTimeReport(events) {
let totalElapse = 0;
const toLog = events.reduce((acum, current) => {
let found = acum.find(el => el.name === current.name);
const currentElapseMs = current.range.end - current.range.start;
totalElapse = totalElapse + currentElapseMs;
if (found)
found.elapse = found.elapse + currentElapseMs;
else
acum.push({ name: current.name, elapse: currentElapseMs });
return acum;
}, []).map(nameRange => {
nameRange.percentage = Number(Number(100 * nameRange.elapse / totalElapse).toFixed(2));
nameRange.elapse = Math.floor(nameRange.elapse);
return nameRange;
});
console.log('');
console.log('Total elapse Time of each event: ');
consoleTable(toLog);
return events;
}
function coincidingEventsReport(elapseTable) {
R.pipe(groupByWithCalc((row) => JSON.stringify(row.runningEvents.sort(arraySorter())), { percentage: (l, r) => (l ?? 0) + r, elapseMs: (l, r) => (l ?? 0) + r }), R.map(row => ({ ...row, elapseMs: Math.floor(row.elapseMs), percentage: Number(row.percentage.toFixed(2)) })), (coincidingEvents) => {
console.log('');
console.log('Coinciding Events timeline: ');
consoleTable(coincidingEvents);
})(elapseTable);
return elapseTable;
}
function logTimeline(timeline) {
console.log('');
console.log('Timeline of events:');
console.log(timeline.draw());
}
function createTimeline(data) {
const timeline = Table(data);
timeline.addColumn({ type: Text(), id: 'event', title: 'Events' });
timeline.addColumn({ type: Timeline(), id: 'ranges' });
return timeline;
}
function formatReportAndReturnInputParam(data) {
let toReport = Object.entries(data).map(([eventName, event]) => ({
event: eventName,
ranges: event.ranges.map(({ start, end }) => ({ start: Math.floor(start), end: Math.floor(end) }))
}));
const toLog = createTimeline(toReport);
logTimeline(toLog);
return data;
}
function timelineLines() {
let toReport = Object.entries(historyTimeIntervals).map(([eventName, event]) => ({
event: eventName,
ranges: event.ranges.map(({ start, end }) => ({ start: Math.floor(start), end: Math.floor(end) }))
}));
return createTimeline(toReport).draw();
}
function chronoReport() {
console.log('');
Object.entries(chronoEvents).forEach(([key, value]) => console.log(key, ': ', value.date));
}
function report() {
createTimeEvent('report');
chronoReport();
R.pipe(
//RE.RLog('0-->: '),
formatReportAndReturnInputParam, eventsReport, historyToListOfNameRanges,
//RE.RLog('1-->: '),
totalEventsElapseTimeReport,
//RE.RLog('2-->: '),
compactListOfNameRanges,
//RE.RLog('3-->: '),
R.sort(sorterByPaths('range.start')), reportListOfNameRanges,
//RE.RLog('4-->: '),
coincidingEventsReport)(historyTimeIntervals);
}
function historyToListOfNameRanges(historyTimeIntervals) {
return Object.entries(historyTimeIntervals)
.reduce((acum, [key, value]) => {
acum.push(...(value.ranges?.map(range => ({ name: key, range }))) ?? []);
return acum;
}, []);
}
function compactListOfNameRanges(ListOfRangeNames) {
return ListOfRangeNames.reduce((acum, { name, range }) => {
acum.push({ name, isLeft: true, edge: range.start, edgeEnd: range.end });
acum.push({ name, isLeft: false, edge: range.end });
return acum;
}, [])
.sort(sorterByPaths('edge'))
.reduce((acum, { name, isLeft, edge, edgeEnd }, index, table) => {
if (isLeft) {
let i = index;
do {
pushUniqueKeyOrChange({ runningEvents: [name], range: rangeType(table[i].edge, table[i + 1].edge) }, acum, ['range'], (newRow, existingRow) => {
pushUniqueKey(name, existingRow.runningEvents);
return existingRow;
});
i++;
} while (!(table[i].name === name && table[i].isLeft === false && table[i].edge === edgeEnd));
}
return acum;
}, []).filter(elem => elem.range.start !== elem.range.end);
}
function reportListOfNameRanges(listOfNameRanges) {
let totalElapse = 0;
return listOfNameRanges.map(({ runningEvents, range }) => {
let elapseMs = milisecondsRangeToElapseMs(range);
totalElapse = totalElapse + elapseMs;
return {
runningEvents,
elapseMs
};
}).map(nameRange => {
nameRange.percentage = 100 * nameRange.elapseMs / totalElapse;
return nameRange;
});
}
const setTime = event => data => {
time(event);
return data;
};
const setTimeEnd = event => data => {
timeEnd(event);
return data;
};
const logReport = data => {
report();
return data;
};
const getChronoState = () => historyTimeIntervals;
const setChronoStateUsingPerformanceAPIFormat = (performanceGetEntriesByTypeOjb) => {
historyTimeIntervals =
performanceGetEntriesByTypeOjb.reduce((historyAcum, { name, startTime, duration, entryType }) => {
validateEventName(name);
if (entryType === 'mark') {
historyAcum[name] ??= {};
historyAcum[name].start ??= [];
historyAcum[name].start.push(startTime);
}
if (entryType === 'measure') {
historyAcum[name] ??= {};
historyAcum[name].ranges ??= [];
historyAcum[name].ranges.push(rangeType(startTime, startTime + duration));
}
return historyAcum;
}, {});
};
const getChronoStateUsingPerformanceAPIFormat = () => {
return Object.entries(historyTimeIntervals).reduce((performanceAPIFormatAcum, [eventName, eventValue]) => {
eventValue.start?.forEach(start => performanceAPIFormatAcum.push({
duration: 0,
startTime: start,
name: eventName,
entryType: 'mark'
}));
eventValue.ranges?.forEach(range => performanceAPIFormatAcum.push({
duration: range.end - range.start,
startTime: range.start,
name: eventName,
entryType: 'measure'
}));
return performanceAPIFormatAcum;
}, []);
};
function reset() {
historyTimeIntervals = {};
chronoEvents = { chronoCreation: chronoEvents['chronoCreation'] };
}
return {
time, timeEnd, report, setTime, setTimeEnd, logReport, timelineLines,
getChronoState, setChronoStateUsingPerformanceAPIFormat, getChronoStateUsingPerformanceAPIFormat, average,
reset
};
}
function milisecondsRangeToElapseMs({ start, end }) {
return end - start;
}
function Range(...params) {
let type;
let displayFormat;
let referenceMiliseconds;
if (params.length >= 2) {
return range(...params);
}
else {
({ type, displayFormat, referenceMiliseconds } = params[0]);
return range;
}
function range(start, end, interval) {
//console.log(interval)
if (start > end)
throw new Error('range(start, end) start cannot be > than end');
function toString() {
if (type === 'miliseconds' && displayFormat === 'ms' && referenceMiliseconds !== undefined) {
const startMs = milisecondsRangeToElapseMs({ start: referenceMiliseconds, end: start });
const endMs = milisecondsRangeToElapseMs({ start: referenceMiliseconds, end });
return `${'interval: ' + interval} { start:${startMs} <-${endMs - startMs}-> end:${endMs} }`;
}
return `{ start:${start}, end:${end} }`;
}
function intersect(rangeB) {
let newStart = start > rangeB.start ? start : rangeB.start;
let newEnd = end < rangeB.end ? end : rangeB.end;
if (newStart === undefined || newEnd === undefined)
return range(undefined, undefined);
if (newStart > newEnd)
return range(undefined, undefined);
return range(newStart, newEnd);
}
return {
[Symbol.for('nodejs.util.inspect.custom')]: toString,
toString,
intersect,
start,
end,
interval
};
}
}
export { Chrono };