Skip to content

Commit d0db5d3

Browse files
authored
ESQL: Limit the toString of plan nodes (#145140)
Truncates the `toString` for ESQL's plan nodes when used outside the golden tests to 10 lines of 110 characters each. Why 110? Because that's where we truncated before I got to this code. Why 10 lines? Because we weren't truncating at all before. Just splitting at 110 characters one time. 10 lines of body for each plan node is quite large, really. Imagine a pathologically large plan, like 1000 nodes. That means at worst it's `toString` would be a little more than a megabyte. That's terrible! But I'm still ok logging it.
1 parent e03f6d6 commit d0db5d3

5 files changed

Lines changed: 436 additions & 159 deletions

File tree

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/core/tree/Node.java

Lines changed: 20 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,9 @@
1010
import org.elasticsearch.action.support.SubscribableListener;
1111
import org.elasticsearch.common.io.stream.NamedWriteable;
1212
import org.elasticsearch.xpack.esql.core.QlIllegalArgumentException;
13-
import org.elasticsearch.xpack.esql.core.expression.NameId;
1413
import org.elasticsearch.xpack.esql.core.util.Holder;
1514

1615
import java.util.ArrayList;
17-
import java.util.BitSet;
18-
import java.util.Iterator;
1916
import java.util.List;
2017
import java.util.Objects;
2118
import java.util.function.BiConsumer;
@@ -40,8 +37,18 @@
4037
* @param <T> node type
4138
*/
4239
public abstract class Node<T extends Node<T>> implements NamedWriteable {
40+
/**
41+
* Maximum number of properties rendered by {@link #toString}.
42+
*/
4343
private static final int TO_STRING_MAX_PROP = 10;
44+
/**
45+
* Maximum number of characters per line rendered by {@link #toString}.
46+
*/
4447
public static final int TO_STRING_MAX_WIDTH = 110;
48+
/**
49+
* Maximum number of lines rendered by {@link #toString}.
50+
*/
51+
public static final int TO_STRING_MAX_LINES = 25;
4552

4653
private final Source source;
4754
private final List<T> children;
@@ -66,7 +73,7 @@ public String sourceText() {
6673
return source.text();
6774
}
6875

69-
public List<T> children() {
76+
public final List<T> children() {
7077
return children;
7178
}
7279

@@ -444,16 +451,18 @@ public List<Object> nodeProperties() {
444451

445452
public enum NodeStringFormat {
446453
/** No list truncation, no line breaks due to string width. */
447-
FULL(Integer.MAX_VALUE, Integer.MAX_VALUE),
454+
FULL(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE),
448455
/** List truncation and line breaks due to string width applied. */
449-
LIMITED(TO_STRING_MAX_PROP, TO_STRING_MAX_WIDTH);
456+
LIMITED(TO_STRING_MAX_PROP, TO_STRING_MAX_WIDTH, TO_STRING_MAX_LINES);
450457

451-
private final int maxProperties;
452-
private final int maxWidth;
458+
final int maxProperties;
459+
final int maxWidth;
460+
final int maxLines;
453461

454-
NodeStringFormat(int maxProperties, int maxWidth) {
462+
NodeStringFormat(int maxProperties, int maxWidth, int maxLines) {
455463
this.maxProperties = maxProperties;
456464
this.maxWidth = maxWidth;
465+
this.maxLines = maxLines;
457466
}
458467
}
459468

@@ -476,130 +485,11 @@ public String toString() {
476485
}
477486

478487
public String toString(NodeStringFormat format) {
479-
return treeString(new StringBuilder(), 0, new BitSet(), format).toString();
488+
return new NodeToString(format).treeString(this, 0).toString();
480489
}
481490

482-
/**
483-
* Render this {@link Node} as a tree like
484-
* <pre>
485-
* {@code
486-
* Project[[i{f}#0]]
487-
* \_Filter[i{f}#1]
488-
* \_SubQueryAlias[test]
489-
* \_EsRelation[test][i{f}#2]
490-
* }
491-
* </pre>
492-
*/
493-
final StringBuilder treeString(StringBuilder sb, int depth, BitSet hasParentPerDepth, NodeStringFormat format) {
494-
if (depth > 0) {
495-
// draw children
496-
for (int column = 0; column < depth; column++) {
497-
if (hasParentPerDepth.get(column)) {
498-
sb.append("|");
499-
// if not the last elder, adding padding (since each column has two chars ("|_" or "\_")
500-
if (column < depth - 1) {
501-
sb.append(" ");
502-
}
503-
} else {
504-
// if the child has no parent (elder on the previous level), it means its the last sibling
505-
sb.append((column == depth - 1) ? "\\" : " ");
506-
}
507-
}
508-
509-
sb.append("_");
510-
}
511-
512-
sb.append(nodeString(format));
513-
514-
@SuppressWarnings("HiddenField")
515-
List<T> children = children();
516-
if (children.isEmpty() == false) {
517-
sb.append("\n");
518-
}
519-
for (int i = 0; i < children.size(); i++) {
520-
T t = children.get(i);
521-
hasParentPerDepth.set(depth, i < children.size() - 1);
522-
t.treeString(sb, depth + 1, hasParentPerDepth, format);
523-
if (i < children.size() - 1) {
524-
sb.append("\n");
525-
}
526-
}
527-
return sb;
528-
}
529-
530-
/**
531-
* Render the properties of this {@link Node} one by
532-
* one like {@code foo bar baz}. These go inside the
533-
* {@code [} and {@code ]} of the output of {@link #treeString}.
534-
*/
535491
protected String propertiesToString(boolean skipIfChild, NodeStringFormat format) {
536-
StringBuilder sb = new StringBuilder();
537-
538-
@SuppressWarnings("HiddenField")
539-
List<?> children = children();
540-
// eliminate children (they are rendered as part of the tree)
541-
int remainingProperties = format.maxProperties;
542-
int currentMaxWidth = 0;
543-
boolean needsComma = false;
544-
545-
List<Object> props = nodeProperties();
546-
for (Object prop : props) {
547-
// consider a property if it is not ignored AND
548-
// it's not a child (optional)
549-
if ((skipIfChild && (children.contains(prop) || children.equals(prop))) == false) {
550-
if (remainingProperties-- < 0) {
551-
sb.append("...").append(props.size() - format.maxProperties).append("fields not shown");
552-
break;
553-
}
554-
555-
if (needsComma) {
556-
sb.append(",");
557-
}
558-
559-
String stringValue = toString(prop, format);
560-
561-
// : Objects.toString(prop);
562-
if (currentMaxWidth + stringValue.length() > format.maxWidth) {
563-
int cutoff = Math.max(0, format.maxWidth - currentMaxWidth);
564-
sb.append(stringValue, 0, cutoff);
565-
sb.append("\n");
566-
stringValue = stringValue.substring(cutoff);
567-
currentMaxWidth = 0;
568-
}
569-
currentMaxWidth += stringValue.length();
570-
sb.append(stringValue);
571-
572-
needsComma = true;
573-
}
574-
}
575-
576-
return sb.toString();
577-
}
578-
579-
private static String toString(Object obj, NodeStringFormat format) {
580-
StringBuilder sb = new StringBuilder();
581-
toString(sb, obj, format);
582-
return sb.toString();
583-
}
584-
585-
private static void toString(StringBuilder sb, Object obj, NodeStringFormat format) {
586-
if (obj instanceof Iterable<?> iterable) {
587-
sb.append("[");
588-
for (Iterator<?> it = iterable.iterator(); it.hasNext();) {
589-
Object o = it.next();
590-
toString(sb, o, format);
591-
if (it.hasNext()) {
592-
sb.append(", ");
593-
}
594-
}
595-
sb.append("]");
596-
} else if (obj instanceof Node<?> node) {
597-
sb.append(node.nodeString(format));
598-
} else if (obj instanceof NameId) {
599-
sb.append("#").append(obj);
600-
} else {
601-
sb.append(obj);
602-
}
492+
return new NodePropertiesToString(format, this, skipIfChild).propertiesToString();
603493
}
604494

605495
private <U> boolean containsNull(List<U> us) {
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
package org.elasticsearch.xpack.esql.core.tree;
8+
9+
import org.elasticsearch.xpack.esql.core.expression.NameId;
10+
11+
import java.util.List;
12+
13+
/**
14+
* Renders the properties of a {@link Node} as a string.
15+
*/
16+
class NodePropertiesToString {
17+
private final Node.NodeStringFormat format;
18+
private final Node<?> node;
19+
private final boolean skipIfChild;
20+
private final StringBuilder result = new StringBuilder();
21+
private int charactersRemainingInLine;
22+
private int linesUsed = 0;
23+
24+
NodePropertiesToString(Node.NodeStringFormat format, Node<?> node, boolean skipIfChild) {
25+
this.format = format;
26+
this.node = node;
27+
this.skipIfChild = skipIfChild;
28+
this.charactersRemainingInLine = format.maxWidth;
29+
}
30+
31+
/**
32+
* Render the properties of this {@link Node} one by
33+
* one like {@code foo bar baz}. These go inside the
34+
* {@code [} and {@code ]} of the output of {@link NodeToString#treeString}.
35+
*/
36+
String propertiesToString() {
37+
List<Object> props = node.nodeProperties();
38+
int remainingProperties = format.maxProperties;
39+
boolean firstProperty = true;
40+
for (Object prop : props) {
41+
// if skipping children, check skip if this is a child
42+
if (skipIfChild && (node.children().contains(prop) || node.children().equals(prop))) {
43+
continue;
44+
}
45+
if (remainingProperties-- < 0) {
46+
result.append("...").append(props.size() - format.maxProperties).append("fields not shown");
47+
break;
48+
}
49+
50+
if (firstProperty) {
51+
firstProperty = false;
52+
} else {
53+
appendString(",");
54+
}
55+
boolean canContinue = prop instanceof Iterable<?> iterable ? appendIterable(iterable) : appendString(propertyToString(prop));
56+
if (canContinue == false) {
57+
break;
58+
}
59+
}
60+
61+
return result.toString();
62+
}
63+
64+
/**
65+
* Append {@code stringValue} to {@link #result}, wrapping at line boundaries.
66+
* Returns {@code true} if rendering can continue, {@code false} if the line budget is exhausted.
67+
*/
68+
private boolean appendString(String stringValue) {
69+
int start = 0;
70+
while (stringValue.length() - start > charactersRemainingInLine) {
71+
result.append(stringValue, start, start + charactersRemainingInLine);
72+
if (linesUsed >= format.maxLines - 1) {
73+
result.append("...");
74+
return false;
75+
}
76+
result.append("\n");
77+
linesUsed++;
78+
start += charactersRemainingInLine;
79+
charactersRemainingInLine = format.maxWidth;
80+
}
81+
result.append(stringValue, start, stringValue.length());
82+
charactersRemainingInLine -= stringValue.length() - start;
83+
return true;
84+
}
85+
86+
private boolean appendIterable(Iterable<?> iterable) {
87+
if (appendString("[") == false) {
88+
return false;
89+
}
90+
boolean firstElement = true;
91+
for (Object element : iterable) {
92+
if (firstElement == false) {
93+
if (appendString(", ") == false) {
94+
return false;
95+
}
96+
}
97+
if (appendString(propertyToString(element)) == false) {
98+
return false;
99+
}
100+
firstElement = false;
101+
}
102+
return appendString("]");
103+
}
104+
105+
private String propertyToString(Object obj) {
106+
return switch (obj) {
107+
case null -> "null";
108+
case Node<?> n -> n.nodeString(format);
109+
case NameId nameId -> "#" + obj;
110+
default -> String.valueOf(obj);
111+
};
112+
}
113+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
package org.elasticsearch.xpack.esql.core.tree;
8+
9+
import java.util.BitSet;
10+
import java.util.List;
11+
12+
/**
13+
* Renders {@link Node} trees as strings.
14+
*/
15+
class NodeToString {
16+
private final StringBuilder sb = new StringBuilder();
17+
private final BitSet hasParentPerDepth = new BitSet();
18+
private final Node.NodeStringFormat format;
19+
20+
NodeToString(Node.NodeStringFormat format) {
21+
this.format = format;
22+
}
23+
24+
/**
25+
* Render this {@link Node} as a tree like
26+
* {@snippet lang=txt :
27+
* Project[[i{f}#0]]
28+
* \_Filter[i{f}#1]
29+
* \_SubQueryAlias[test]
30+
* \_EsRelation[test][i{f}#2]
31+
* }
32+
*/
33+
StringBuilder treeString(Node<?> node, int depth) {
34+
indent(depth);
35+
36+
sb.append(node.nodeString(format));
37+
38+
List<? extends Node<?>> children = node.children();
39+
if (children.isEmpty() == false) {
40+
sb.append("\n");
41+
}
42+
for (int i = 0; i < children.size(); i++) {
43+
Node<?> t = children.get(i);
44+
hasParentPerDepth.set(depth, i < children.size() - 1);
45+
treeString(t, depth + 1);
46+
if (i < children.size() - 1) {
47+
sb.append("\n");
48+
}
49+
}
50+
return sb;
51+
}
52+
53+
private void indent(int depth) {
54+
if (depth == 0) {
55+
return;
56+
}
57+
// draw children
58+
for (int column = 0; column < depth; column++) {
59+
if (hasParentPerDepth.get(column)) {
60+
sb.append("|");
61+
// if not the last elder, adding padding (since each column has two chars ("|_" or "\_")
62+
if (column < depth - 1) {
63+
sb.append(" ");
64+
}
65+
} else {
66+
// if the child has no parent (elder on the previous level), it means its the last sibling
67+
sb.append((column == depth - 1) ? "\\" : " ");
68+
}
69+
}
70+
sb.append("_");
71+
}
72+
73+
}

0 commit comments

Comments
 (0)