Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

util: improve inspect compact #26269

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,9 @@ stream.write('With ES6');
<!-- YAML
added: v0.3.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/26269
description: The `compact` option accepts numbers for a new output mode.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/24971
description: Internal properties no longer appear in the context argument
Expand Down Expand Up @@ -461,11 +464,13 @@ changes:
* `breakLength` {integer} The length at which an object's keys are split
across multiple lines. Set to `Infinity` to format an object as a single
line. **Default:** `60` for legacy compatibility.
* `compact` {boolean} Setting this to `false` causes each object key to
be displayed on a new line. It will also add new lines to text that is
longer than `breakLength`. Note that no text will be reduced below 16
characters, no matter the `breakLength` size. For more information, see the
example below. **Default:** `true`.
* `compact` {boolean|integer} Setting this to `false` causes each object key
to be displayed on a new line. It will also add new lines to text that is
longer than `breakLength`. If set to a number, the most `n` inner elements
are united on a single line as long as all properties fit into
`breakLength`. Short array elements are also grouped together. Note that no
text will be reduced below 16 characters, no matter the `breakLength` size.
For more information, see the example below. **Default:** `true`.
* `sorted` {boolean|Function} If set to `true` or a function, all properties
of an object, and `Set` and `Map` entries are sorted in the resulting
string. If set to `true` the [default sort][] is used. If set to a function,
Expand Down
180 changes: 154 additions & 26 deletions lib/internal/util/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ function inspect(value, opts) {
budget: {},
indentationLvl: 0,
seen: [],
currentDepth: 0,
stylize: stylizeNoColor,
showHidden: inspectDefaultOptions.showHidden,
depth: inspectDefaultOptions.depth,
Expand Down Expand Up @@ -769,6 +770,7 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
recurseTimes += 1;

ctx.seen.push(value);
ctx.currentDepth = recurseTimes;
let output;
const indentationLvl = ctx.indentationLvl;
try {
Expand All @@ -792,7 +794,37 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
}
}

const res = reduceToSingleString(ctx, output, base, braces);
let combine = false;
if (typeof ctx.compact === 'number') {
// Memorize the original output length. In case the the output is grouped,
// prevent lining up the entries on a single line.
const entries = output.length;
// Group array elements together if the array contains at least six separate
// entries.
if (extrasType === kArrayExtrasType && output.length > 6) {
output = groupArrayElements(ctx, output);
}
// `ctx.currentDepth` is set to the most inner depth of the currently
// inspected object part while `recurseTimes` is the actual current depth
// that is inspected.
//
// Example:
//
// const a = { first: [ 1, 2, 3 ], second: { inner: [ 1, 2, 3 ] } }
//
// The deepest depth of `a` is 2 (a.second.inner) and `a.first` has a max
// depth of 1.
//
// Consolidate all entries of the local most inner depth up to
// `ctx.compact`, as long as the properties are smaller than
// `ctx.breakLength`.
if (ctx.currentDepth - recurseTimes < ctx.compact &&
entries === output.length) {
combine = true;
}
}

const res = reduceToSingleString(ctx, output, base, braces, combine);
const budget = ctx.budget[ctx.indentationLvl] || 0;
const newLength = budget + res.length;
ctx.budget[ctx.indentationLvl] = newLength;
Expand All @@ -809,6 +841,83 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
return res;
}

function groupArrayElements(ctx, output) {
let totalLength = 0;
let maxLength = 0;
let i = 0;
const dataLen = new Array(output.length);
// Calculate the total length of all output entries and the individual max
// entries length of all output entries. We have to remove colors first,
// otherwise the length would not be calculated properly.
for (; i < output.length; i++) {
const len = ctx.colors ? removeColors(output[i]).length : output[i].length;
dataLen[i] = len;
totalLength += len;
if (maxLength < len)
maxLength = len;
}
// Add two to `maxLength` as we add a single whitespace character plus a comma
// in-between two entries.
const actualMax = maxLength + 2;
// Check if at least three entries fit next to each other and prevent grouping
// of arrays that contains entries of very different length (i.e., if a single
// entry is longer than 1/5 of all other entries combined). Otherwise the
// space in-between small entries would be enormous.
if (actualMax * 3 + ctx.indentationLvl < ctx.breakLength &&
(totalLength / maxLength > 5 || maxLength <= 6)) {

const approxCharHeights = 2.5;
const bias = 1;
// Dynamically check how many columns seem possible.
const columns = Math.min(
// Ideally a square should be drawn. We expect a character to be about 2.5
// times as high as wide. This is the area formula to calculate a square
// which contains n rectangles of size `actualMax * approxCharHeights`.
// Divide that by `actualMax` to receive the correct number of columns.
// The added bias slightly increases the columns for short entries.
Math.round(
Math.sqrt(
approxCharHeights * (actualMax - bias) * output.length
) / (actualMax - bias)
),
// Limit array grouping for small `compact` modes as the user requested
// minimal grouping.
ctx.compact * 3,
// Limit the columns to a maximum of ten.
10
);
// Return with the original output if no grouping should happen.
if (columns <= 1) {
return output;
}
// Calculate the maximum length of all entries that are visible in the first
// column of the group.
const tmp = [];
let firstLineMaxLength = dataLen[0];
for (i = columns; i < dataLen.length; i += columns) {
if (dataLen[i] > firstLineMaxLength)
firstLineMaxLength = dataLen[i];
}
// Each iteration creates a single line of grouped entries.
for (i = 0; i < output.length; i += columns) {
// Calculate extra color padding in case it's active. This has to be done
// line by line as some lines might contain more colors than others.
let colorPadding = output[i].length - dataLen[i];
// Add padding to the first column of the output.
let str = output[i].padStart(firstLineMaxLength + colorPadding, ' ');
// The last lines may contain less entries than columns.
const max = Math.min(i + columns, output.length);
for (var j = i + 1; j < max; j++) {
colorPadding = output[j].length - dataLen[j];
str += `, ${output[j].padStart(maxLength + colorPadding, ' ')}`;
}
tmp.push(str);
}
output = tmp;
}
return output;
}

function handleMaxCallStackSize(ctx, err, constructor, tag, indentationLvl) {
if (isStackOverflowError(err)) {
ctx.seen.pop();
Expand All @@ -833,7 +942,7 @@ function formatBigInt(fn, value) {

function formatPrimitive(fn, value, ctx) {
if (typeof value === 'string') {
if (ctx.compact === false &&
if (ctx.compact !== true &&
ctx.indentationLvl + value.length > ctx.breakLength &&
value.length > kMinLineLength) {
const rawMaxLineLength = ctx.breakLength - ctx.indentationLvl;
Expand Down Expand Up @@ -1143,7 +1252,7 @@ function formatProperty(ctx, value, recurseTimes, key, type) {
const desc = Object.getOwnPropertyDescriptor(value, key) ||
{ value: value[key], enumerable: true };
if (desc.value !== undefined) {
const diff = (type !== kObjectType || ctx.compact === false) ? 2 : 3;
const diff = (type !== kObjectType || ctx.compact !== true) ? 2 : 3;
ctx.indentationLvl += diff;
str = formatValue(ctx, desc.value, recurseTimes);
if (diff === 3) {
Expand Down Expand Up @@ -1200,39 +1309,58 @@ function formatProperty(ctx, value, recurseTimes, key, type) {
return `${name}:${extra}${str}`;
}

function reduceToSingleString(ctx, output, base, braces) {
const breakLength = ctx.breakLength;
let i = 0;
if (ctx.compact === false) {
const indentation = ' '.repeat(ctx.indentationLvl);
let res = `${base ? `${base} ` : ''}${braces[0]}\n${indentation} `;
for (; i < output.length - 1; i++) {
res += `${output[i]},\n${indentation} `;
function isBelowBreakLength(ctx, output, start) {
// Each entry is separated by at least a comma. Thus, we start with a total
// length of at least `output.length`. In addition, some cases have a
// whitespace in-between each other that is added to the total as well.
let totalLength = output.length + start;
if (totalLength + output.length > ctx.breakLength)
return false;
for (var i = 0; i < output.length; i++) {
if (ctx.colors) {
totalLength += removeColors(output[i]).length;
} else {
totalLength += output[i].length;
}
if (totalLength > ctx.breakLength) {
return false;
}
res += `${output[i]}\n${indentation}${braces[1]}`;
return res;
}
if (output.length * 2 <= breakLength) {
let length = 0;
for (; i < output.length && length <= breakLength; i++) {
if (ctx.colors) {
length += removeColors(output[i]).length + 1;
} else {
length += output[i].length + 1;
return true;
}

function reduceToSingleString(ctx, output, base, braces, combine = false) {
if (ctx.compact !== true) {
if (combine) {
// Line up all entries on a single line in case the entries do not exceed
// `breakLength`. Add 10 as constant to start next to all other factors
// that may reduce `breakLength`.
const start = output.length + ctx.indentationLvl +
braces[0].length + base.length + 10;
if (isBelowBreakLength(ctx, output, start)) {
return `${base ? `${base} ` : ''}${braces[0]} ${join(output, ', ')} ` +
braces[1];
}
}
if (length <= breakLength)
return `${braces[0]}${base ? ` ${base}` : ''} ${join(output, ', ')} ` +
braces[1];
// Line up each entry on an individual line.
const indentation = `\n${' '.repeat(ctx.indentationLvl)}`;
return `${base ? `${base} ` : ''}${braces[0]}${indentation} ` +
`${join(output, `,${indentation} `)}${indentation}${braces[1]}`;
}
// Line up all entries on a single line in case the entries do not exceed
// `breakLength`.
if (isBelowBreakLength(ctx, output, 0)) {
return `${braces[0]}${base ? ` ${base}` : ''} ${join(output, ', ')} ` +
braces[1];
}
const indentation = ' '.repeat(ctx.indentationLvl);
// If the opening "brace" is too large, like in the case of "Set {",
// we need to force the first item to be on the next line or the
// items will not line up correctly.
const indentation = ' '.repeat(ctx.indentationLvl);
const ln = base === '' && braces[0].length === 1 ?
' ' : `${base ? ` ${base}` : ''}\n${indentation} `;
const str = join(output, `,\n${indentation} `);
return `${braces[0]}${ln}${str} ${braces[1]}`;
// Line up each entry on an individual line.
return `${braces[0]}${ln}${join(output, `,\n${indentation} `)} ${braces[1]}`;
}

module.exports = {
Expand Down
Loading