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

[Performance] Export Model constructor throws exceptions on very hot paths #127

Merged
merged 10 commits into from
Sep 6, 2017
Merged
Show file tree
Hide file tree
Changes from 4 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
14 changes: 12 additions & 2 deletions core/src/main/java/org/kohsuke/stapler/export/ModelBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,22 @@ public class ModelBuilder {
/*package*/ final Map<Class, Model> models = new ConcurrentHashMap<Class, Model>();

public <T> Model<T> get(Class<T> type) throws NotExportableException {
if (type.getAnnotation(ExportedBean.class) == null) {
throw new NotExportableException(type);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this not redundant? AFAICT the method it delegates to does the same thing.

}
return get(type, null, null);
}

public <T> Model<T> get(Class<T> type, @CheckForNull Class<?> propertyOwner, @Nullable String property) throws NotExportableException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why removing the throws clause?

public <T> Model<T> get(Class<T> type, @CheckForNull Class<?> propertyOwner, @Nullable String property) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Nonnull return

if (type.getAnnotation(ExportedBean.class) == null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you rather meant to write

Model<T> model = getOrNull(type, propertyOwner, property);
if (model == null) {
    throw new NotExportableException(type);
}
return model;

throw new NotExportableException(type);
}
return getOrNull(type, propertyOwner, property);
}

public <T> Model<T> getOrNull(Class<T> type, @CheckForNull Class<?> propertyOwner, @Nullable String property) throws NotExportableException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CheckForNull return. And why is this declared to throw NotExportableException?

Model m = models.get(type);
if(m==null) {
if(m==null && type.getAnnotation(ExportedBean.class) != null) {
m = new Model<T>(this, type, propertyOwner, property);
}
return m;
Expand Down
209 changes: 108 additions & 101 deletions core/src/main/java/org/kohsuke/stapler/export/Property.java
Original file line number Diff line number Diff line change
Expand Up @@ -131,23 +131,22 @@ public void writeTo(Object object, TreePruner pruner, DataWriter writer) throws
TreePruner child = pruner.accept(object, this);
if (child==null) return;

Object d = writer.getExportConfig().getExportInterceptor().getValue(this,object, writer.getExportConfig());
Object d = writer.getExportConfig().getExportInterceptor().getValue(this,object,writer.getExportConfig());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gratuitous, revert


if ((d==null && skipNull) || d == ExportInterceptor.SKIP) { // don't write anything
return;
}
if (merge) {
// merged property will get all its properties written here
if (d != null) {
Model model;
try {
model = owner.get(d.getClass(), parent.type, name);
} catch (NotExportableException e) {
if (d.getClass().getAnnotation(ExportedBean.class) == null) {
if(writer.getExportConfig().isSkipIfFail()){
return;
} else {
throw new NotExportableException(d.getClass());
}
throw e;
}
Model model = owner.getOrNull(d.getClass(), parent.type, name);
model.writeNestedObjectTo(d, new FilteringTreePruner(parent.HAS_PROPERTY_NAME_IN_ANCESTRY,child), writer);
}
} else {
Expand Down Expand Up @@ -199,118 +198,126 @@ private void writeValue(Type expected, Object value, TreePruner pruner, DataWrit

Class c = value.getClass();

Model model;
try {
model = owner.get(c, parent.type, name);
} catch (NotExportableException ex) {
if(STRING_TYPES.contains(c)) {
writer.value(value.toString());
return;
}
if(PRIMITIVE_TYPES.contains(c)) {
writer.valuePrimitive(value);
return;
if (c.getAnnotation(ExportedBean.class) == null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I missing something or would this not be caught by the null return value from getOrNull a few lines later anyway?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are absolutely right. Fixed.

handleNotExportable(expected, value, pruner, writer, skipIfFail, c);
} else {
try {
writer.type(expected, value.getClass());
} catch (AbstractMethodError _) {
// legacy impl that doesn't understand it
}
Class act = c.getComponentType();
if (act !=null) { // array
Range r = pruner.getRange();
writer.startArray();
if (value instanceof Object[]) {
// typical case
for (Object item : r.apply((Object[]) value)) {
writeBuffered(act, item, pruner, writer);
}
} else {
// more generic case
int len = Math.min(r.max, Array.getLength(value));
for (int i=r.min; i<len; i++) {
writeBuffered(act, Array.get(value, i), pruner, writer);
}
try {
Model model = owner.getOrNull(c, parent.type, name);
if (model == null) {
throw new NotExportableException(c);
}
writer.endArray();
writer.startObject();
model.writeNestedObjectTo(value, pruner, writer);
writer.endObject();
} catch (NotExportableException ex) {
handleNotExportable(expected, value, pruner, writer, skipIfFail, c);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reduced diff is easier to read here (generally dislike patches that change indentation of large existing code blocks but sometimes it is hard to avoid)

return;
}
if(value instanceof Iterable) {
writer.startArray();
Type expectedItemType = Types.getTypeArgument(expected, 0, null);
for (Object item : pruner.getRange().apply((Iterable) value)) {
writeBuffered(expectedItemType, item, pruner, writer);
}
}

private void handleNotExportable(Type expected, Object value, TreePruner pruner, DataWriter writer, boolean skipIfFail, Class c) throws IOException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method name is potentially misleading. Really this method is handling anything without @ExportedBean, but that includes all sorts of objects which are “exportable”. It is only at the very end, starting where we check skipIfFail, that this method is in fact handling objects which are not exportable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed and added a description.

if(STRING_TYPES.contains(c)) {
writer.value(value.toString());
return;
}
if(PRIMITIVE_TYPES.contains(c)) {
writer.valuePrimitive(value);
return;
}
Class act = c.getComponentType();
if (act !=null) { // array
Range r = pruner.getRange();
writer.startArray();
if (value instanceof Object[]) {
// typical case
for (Object item : r.apply((Object[]) value)) {
writeBuffered(act, item, pruner, writer);
}
writer.endArray();
return;
} else {
// more generic case
int len = Math.min(r.max, Array.getLength(value));
for (int i=r.min; i<len; i++) {
writeBuffered(act, Array.get(value, i), pruner, writer);
}
}
writer.endArray();
return;
}
if(value instanceof Iterable) {
writer.startArray();
Type expectedItemType = Types.getTypeArgument(expected, 0, null);
for (Object item : pruner.getRange().apply((Iterable) value)) {
writeBuffered(expectedItemType, item, pruner, writer);
}
if(value instanceof Map) {
if (verboseMap!=null) {// verbose form
writer.startArray();
for (Map.Entry e : ((Map<?,?>) value).entrySet()) {
BufferedDataWriter buffer = new BufferedDataWriter(writer.getExportConfig());
try {
writeStartObjectNullType(buffer);
buffer.name(verboseMap[0]);
writeValue(null, e.getKey(), pruner, buffer);
buffer.name(verboseMap[1]);
writeValue(null, e.getValue(), pruner, buffer);
buffer.endObject();
buffer.finished();
} catch (IOException x) {
if (x.getCause() instanceof InvocationTargetException) {
LOGGER.log(Level.WARNING, "skipping export of " + e, x);
}
writer.endArray();
return;
}
if(value instanceof Map) {
if (verboseMap!=null) {// verbose form
writer.startArray();
for (Map.Entry e : ((Map<?,?>) value).entrySet()) {
BufferedDataWriter buffer = new BufferedDataWriter(writer.getExportConfig());
try {
writeStartObjectNullType(buffer);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard to follow this long diff, but IIUC you are basically just extracting handleNotExportable and then calling it twice, right? Pretty much unreviewable so I hope the test coverage here is good.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually the whitespace-reduced diff is more approachable; there must have been a change in indentation for a long block of code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually, Im going to revert the method extraction. Still too long for my taste but should preserve the diff.

buffer.name(verboseMap[0]);
writeValue(null, e.getKey(), pruner, buffer);
buffer.name(verboseMap[1]);
writeValue(null, e.getValue(), pruner, buffer);
buffer.endObject();
buffer.finished();
} catch (IOException x) {
if (x.getCause() instanceof InvocationTargetException) {
LOGGER.log(Level.WARNING, "skipping export of " + e, x);
}
buffer.commit(writer);
}
writer.endArray();
} else {// compact form
writeStartObjectNullType(writer);
for (Map.Entry e : ((Map<?,?>) value).entrySet()) {
BufferedDataWriter buffer = new BufferedDataWriter(writer.getExportConfig());
try {
buffer.name(e.getKey().toString());
writeValue(null, e.getValue(), pruner, buffer);
buffer.finished();
} catch (IOException x) {
if (x.getCause() instanceof InvocationTargetException) {
LOGGER.log(Level.WARNING, "skipping export of " + e, x);
}
buffer.commit(writer);
}
writer.endArray();
} else {// compact form
writeStartObjectNullType(writer);
for (Map.Entry e : ((Map<?,?>) value).entrySet()) {
BufferedDataWriter buffer = new BufferedDataWriter(writer.getExportConfig());
try {
buffer.name(e.getKey().toString());
writeValue(null, e.getValue(), pruner, buffer);
buffer.finished();
} catch (IOException x) {
if (x.getCause() instanceof InvocationTargetException) {
LOGGER.log(Level.WARNING, "skipping export of " + e, x);
}
buffer.commit(writer);
}
writer.endObject();
buffer.commit(writer);
}
return;
}
if(value instanceof Date) {
writer.valuePrimitive(((Date) value).getTime());
return;
}
if(value instanceof Calendar) {
writer.valuePrimitive(((Calendar) value).getTimeInMillis());
return;
}
if(value instanceof Enum) {
writer.value(value.toString());
return;
}

if (skipIfFail) {
writer.startObject();
writer.endObject();
return;
}

throw ex;
return;
}

try {
writer.type(expected, value.getClass());
} catch (AbstractMethodError _) {
// legacy impl that doesn't understand it
if(value instanceof Date) {
writer.valuePrimitive(((Date) value).getTime());
return;
}
if(value instanceof Calendar) {
writer.valuePrimitive(((Calendar) value).getTimeInMillis());
return;
}
if(value instanceof Enum) {
writer.value(value.toString());
return;
}

if (skipIfFail) {
writer.startObject();
writer.endObject();
return;
}

writer.startObject();
model.writeNestedObjectTo(value, pruner, writer);
writer.endObject();
throw new NotExportableException(c);
}

private static class BufferedDataWriter implements DataWriter {
Expand Down