@zendesk/react-measure-timing-hooks
Version:
react hooks for measuring time to interactive and time to render of components
436 lines • 17.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateAsciiTimeline = generateAsciiTimeline;
const EVENTS = 'events';
const TIMELINE = 'timeline';
const TIME = 'time (ms)';
const DEFAULT_MAX_WIDTH = 80;
const DEFAULT_GAP_THRESHOLD = 0.2;
// Function to format durations nicely
function formatDuration(duration) {
// Display in milliseconds
return `${Math.round(duration)}`;
}
// Function to generate the gap marker string based on the duration
function generateGapMarker(duration) {
const durationStr = formatDuration(duration);
return `-<⋯ +${durationStr} ⋯>-`;
}
function generateAsciiTimeline(entries, options = {}) {
const maxWidth = options.width ?? DEFAULT_MAX_WIDTH;
const gapThreshold = options.gapThreshold ?? DEFAULT_GAP_THRESHOLD;
if (entries.length === 0) {
return '';
}
// Determine the time range
const minTime = options.startTime ?? Math.min(...entries.map((e) => e.startTime));
const maxTime = Math.max(...entries.map((e) => e.startTime + e.duration));
const totalTime = maxTime - minTime;
// Sort entries by startTime
const sortedEntries = [...entries].sort((a, b) => a.startTime - b.startTime);
const activePeriods = [];
for (const entry of sortedEntries) {
const entryEnd = entry.startTime + entry.duration;
if (activePeriods.length === 0) {
activePeriods.push({ start: entry.startTime, end: entryEnd });
}
else {
const last = activePeriods[activePeriods.length - 1];
if (entry.startTime <= last.end) {
// Overlapping, extend the last period
last.end = Math.max(last.end, entryEnd);
}
else {
// Non-overlapping, add new period
activePeriods.push({ start: entry.startTime, end: entryEnd });
}
}
}
const segments = [];
let current = minTime;
for (const period of activePeriods) {
if (period.start > current) {
// Gap exists
segments.push({ type: 'gap', start: current, end: period.start });
}
// Active period
segments.push({ type: 'active', start: period.start, end: period.end });
current = period.end;
}
// Check for gap after the last active period
if (current < maxTime) {
segments.push({ type: 'gap', start: current, end: maxTime });
}
// Determine which gaps to compress based on threshold
const compressedSegments = [];
let numCompressedGaps = 0;
for (const segment of segments) {
if (segment.type === 'gap') {
const gapDuration = segment.end - segment.start;
if (gapDuration / totalTime > gapThreshold) {
// Compress this gap
compressedSegments.push({
type: 'gap',
start: segment.start,
end: segment.end,
});
numCompressedGaps += 1;
}
else {
// Treat as active to maintain scale
compressedSegments.push({
type: 'active',
start: segment.start,
end: segment.end,
});
}
}
else {
compressedSegments.push(segment);
}
}
// Calculate the total active time
const totalActiveTime = compressedSegments
.filter((seg) => seg.type === 'active')
.reduce((sum, seg) => sum + (seg.end - seg.start), 0);
// Calculate the scale
const availableWidth = maxWidth - numCompressedGaps * 10; // Estimate average gap marker length
const effectiveAvailableWidth = availableWidth > 0 ? availableWidth : maxWidth;
const scale = options.scale ??
Math.max(1, Math.ceil(totalActiveTime / effectiveAvailableWidth));
const timelineSegments = [];
for (const segment of compressedSegments) {
if (segment.type === 'active') {
const duration = segment.end - segment.start;
const length = Math.max(1, Math.floor(duration / scale));
timelineSegments.push({
type: 'active',
start: segment.start,
end: segment.end,
charLength: length,
});
}
else if (segment.type === 'gap') {
const gapDuration = segment.end - segment.start;
const gapMarker = generateGapMarker(gapDuration);
const charLength = gapMarker.length;
timelineSegments.push({
type: 'gap',
start: segment.start,
end: segment.end,
charLength,
gapMarker, // Store the gap marker string
});
}
}
// Calculate the total timeline length
const timelineLength = timelineSegments.reduce((sum, seg) => sum + seg.charLength, 0);
const cumulativeSegments = [];
let cumulativeChar = 0;
for (const seg of timelineSegments) {
const charStart = cumulativeChar;
const charEnd = cumulativeChar + seg.charLength;
cumulativeSegments.push({
...seg,
charStart,
charEnd,
});
cumulativeChar += seg.charLength;
}
// Function to map a given time to character position
const mapTimeToChar = (time, round = 'up') => {
for (const seg of cumulativeSegments) {
if (seg.type === 'active') {
if (time >= seg.start && time < seg.end) {
const relativeTime = time - seg.start;
const relativeChar = round === 'down'
? Math.floor(relativeTime / scale)
: Math.ceil(relativeTime / scale);
return seg.charStart + Math.min(relativeChar, seg.charLength - 1);
}
if (time === seg.end) {
// Map to the last character of active segment
return seg.charEnd - 1;
}
}
else if (seg.type === 'gap' && time >= seg.start && time < seg.end) {
// Map to the start of the gap marker
return seg.charStart;
}
}
// If time is exactly at maxTime
return timelineLength - 1;
};
// Assign events to rows based on event overlaps
const eventRows = [];
const isOverlap = (row, entry) => {
for (const e of row) {
if (!(entry.startTime >= e.startTime + e.duration ||
entry.startTime + entry.duration <= e.startTime)) {
return true;
}
}
return false;
};
// Assign events to rows
for (const entry of sortedEntries) {
let placed = false;
for (const row of eventRows) {
if (!isOverlap(row, entry)) {
row.push(entry);
placed = true;
break;
}
}
if (!placed) {
eventRows.push([entry]);
}
}
// Determine the maximum prefix length
const prefixes = [EVENTS, TIMELINE, TIME];
const maxPrefixLength = prefixes.reduce((max, prefix) => Math.max(max, prefix.length), 0);
// Pad all prefixes to the maximum length
const padPrefix = (prefix) => prefix.padEnd(maxPrefixLength, ' ');
// Initialize label rows for event labels
const labelRows = [];
for (let i = 0; i < eventRows.length; i++) {
labelRows.push([]);
}
// Function to check if label can be placed in a row without overlapping
const canPlaceLabel = (row, start, length) => {
for (let i = start; i < start + length; i++) {
if (i >= row.length)
continue; // Allow extending
if (row[i] !== ' ')
return false;
}
return true;
};
// Function to place label in the first available row
const placeLabel = (label, start) => {
for (let rowIndex = 0; rowIndex < labelRows.length; rowIndex++) {
const row = labelRows[rowIndex];
// Ensure the row is long enough
if (start + label.length > row.length) {
row.length = start + label.length;
for (let i = 0; i < row.length; i++) {
if (row[i] === undefined)
row[i] = ' ';
}
}
if (canPlaceLabel(row, start, label.length)) {
for (let i = 0; i < label.length; i++) {
row[start + i] = label[i];
}
return;
}
}
// If no existing row can accommodate, add a new one
const newRow = [];
newRow.length = start + label.length;
for (let i = 0; i < newRow.length; i++) {
newRow[i] = ' ';
}
for (let i = 0; i < label.length; i++) {
newRow[start + i] = label[i];
}
labelRows.push(newRow);
};
// Place event labels in label rows considering label length
for (let rowIndex = 0; rowIndex < eventRows.length; rowIndex++) {
const row = eventRows[rowIndex];
for (const entry of row) {
const label = (entry.name !== 'self' ? entry.name : entry.entryType) +
(entry.duration > 0 ? `(${entry.duration})` : '');
const labelStartChar = mapTimeToChar(entry.startTime);
// Place the label starting at labelStartChar
placeLabel(label, labelStartChar);
}
}
// Generate timeline lines
const timelineLines = [];
for (let rowIndex = 0; rowIndex < eventRows.length; rowIndex++) {
const row = eventRows[rowIndex];
const timelineRowArray = Array.from({ length: timelineLength }, () => '-');
// Insert event markers
for (const entry of row) {
const startChar = mapTimeToChar(entry.startTime);
// const endChar = mapTimeToChar(entry.startTime + entry.duration)
// const durationChars = Math.max(1, endChar - startChar)
if (entry.duration > 0) {
const durationChars = Math.max(1, Math.floor(entry.duration / scale));
if (durationChars === 1) {
// Represent as '|'
if (startChar < timelineRowArray.length) {
timelineRowArray[startChar] = '|';
}
}
else if (durationChars === 2) {
// Represent as '[]'
if (startChar < timelineRowArray.length) {
timelineRowArray[startChar] = '[';
}
if (startChar + 1 < timelineRowArray.length) {
timelineRowArray[startChar + 1] = ']';
}
}
else {
// Represent as '[+...+]'
if (startChar < timelineRowArray.length) {
timelineRowArray[startChar] = '[';
}
for (let d = 1; d < durationChars - 1; d++) {
if (startChar + d < timelineRowArray.length) {
timelineRowArray[startChar + d] = '+';
}
}
if (startChar + durationChars - 1 < timelineRowArray.length) {
timelineRowArray[startChar + durationChars - 1] = ']';
}
}
}
else if (startChar < timelineRowArray.length) {
// Instantaneous event
timelineRowArray[startChar] = '|';
}
}
// Calculate active periods for the current row
const rowActivePeriods = [];
for (const entry of row) {
const entryEnd = entry.startTime + entry.duration;
if (rowActivePeriods.length === 0) {
rowActivePeriods.push({ start: entry.startTime, end: entryEnd });
}
else {
const last = rowActivePeriods[rowActivePeriods.length - 1];
if (entry.startTime <= last.end) {
// Overlapping, extend the last period
last.end = Math.max(last.end, entryEnd);
}
else {
// Non-overlapping, add new period
rowActivePeriods.push({ start: entry.startTime, end: entryEnd });
}
}
}
// Find gaps in the current row
const rowGaps = [];
let currentTime = minTime;
for (const period of rowActivePeriods) {
if (period.start > currentTime) {
// Gap exists
rowGaps.push({ start: currentTime, end: period.start });
}
currentTime = period.end;
}
// Check for gap after the last active period
if (currentTime < maxTime) {
rowGaps.push({ start: currentTime, end: maxTime });
}
// Insert gap markers specific to the current row
for (const gap of rowGaps) {
const gapDuration = gap.end - gap.start;
if (gapDuration / totalTime > gapThreshold) {
// Compress this gap
const gapMarker = generateGapMarker(gapDuration).slice(0, -1);
let startChar = mapTimeToChar(gap.start, 'down');
while (timelineRowArray[startChar] &&
timelineRowArray[startChar] !== '-') {
startChar += 1;
}
// Insert gapMarker into timelineRowArray at startChar
for (let i = 0; i < gapMarker.length; i++) {
if (startChar + i < timelineRowArray.length &&
timelineRowArray[startChar + i] === '-') {
timelineRowArray[startChar + i] = gapMarker[i];
}
}
}
}
timelineLines.push(`${timelineRowArray.join('').trimEnd()}`);
}
// Generate time label rows
const timeEntries = sortedEntries.map((e) => ({
time: e.startTime,
label: e.startTime.toString(),
}));
// Remove duplicate time entries
const uniqueTimeEntriesMap = {};
for (const entry of timeEntries) {
uniqueTimeEntriesMap[entry.time] = entry.label;
}
const uniqueTimeEntries = Object.keys(uniqueTimeEntriesMap)
.map((k) => Number.parseInt(k, 10))
.sort((a, b) => a - b)
.map((k) => ({ time: k, label: uniqueTimeEntriesMap[k] }));
// Assign time labels to rows to ensure at least 1 space between labels
const timeLabelRows = [];
for (const entry of uniqueTimeEntries) {
const pos = mapTimeToChar(entry.time);
const { label } = entry;
const startPos = pos;
const endPos = pos + label.length - 1;
// Attempt to place the label in existing rows
let placed = false;
for (const row of timeLabelRows) {
let canPlace = true;
for (let i = 0; i < label.length; i++) {
const currentPos = startPos + i;
if (row[currentPos] && row[currentPos] !== ' ') {
canPlace = false;
break;
}
}
// Check for at least one space before and after
if (canPlace) {
if (startPos > 0 && row[startPos - 1] && row[startPos - 1] !== ' ') {
canPlace = false;
}
if (endPos < row.length - 1 &&
row[endPos + 1] &&
row[endPos + 1] !== ' ') {
canPlace = false;
}
}
if (canPlace) {
// Extend the row if necessary
if (startPos + label.length > row.length) {
row.length = startPos + label.length;
for (let i = 0; i < row.length; i++) {
if (row[i] === undefined)
row[i] = ' ';
}
}
for (let i = 0; i < label.length; i++) {
row[startPos + i] = label[i];
}
placed = true;
break;
}
}
if (!placed) {
// Create a new row for the label
const newRow = [];
newRow.length = startPos + label.length;
for (let i = 0; i < newRow.length; i++) {
newRow[i] = ' ';
}
for (let i = 0; i < label.length; i++) {
newRow[startPos + i] = label[i];
}
timeLabelRows.push(newRow);
}
}
// Generate event label lines with padded prefixes
const eventLabelLines = labelRows.map((row) => `${padPrefix(EVENTS)} | ${row.join('')}`.trimEnd());
// Generate timeline label lines with padded prefixes
const timelineLabelLines = timelineLines.map((line) => `${padPrefix(TIMELINE)} | ${line}`);
// Generate time label lines with padded prefixes
const timeLabelLines = timeLabelRows.map((row) => `${padPrefix(TIME)} | ${row.join('')}`.trimEnd());
// Combine all parts
const allLabelLines = eventLabelLines.reverse().join('\n');
const allTimelineLines = timelineLabelLines.join('\n');
const allTimeLabelLines = timeLabelLines.join('\n');
return `${allLabelLines}\n${allTimelineLines}\n${allTimeLabelLines}`;
}
//# sourceMappingURL=generateAsciiTimeline.js.map