KEMBAR78
Simplify java assertions and add messages to all by krk · Pull Request #1027 · async-profiler/async-profiler · GitHub
Skip to content

Conversation

@krk
Copy link
Contributor

@krk krk commented Oct 15, 2024

Description

Improve Assert.java readability, maintainability and extensibility. Introduce a new isAsserted function which isLess etc. calls with a Comparison enum. Logging and assertion is done in a single place.

Motivation and context

Accept a message parameter in all Assert.java is* methods and move assertion checks to a single method.

How has this been tested?

make test
make test LOG_LEVEL=FINE

Sample fine logs:

FINE: isAsserted false, 'sampled all threads 1/4': 40814.0 > 0.0
FINE: isAsserted false, 'sampled all threads 2/4': 40812.0 > 0.0
FINE: isAsserted false, 'sampled all threads 3/4': 40791.0 > 0.0
FINE: isAsserted false, 'sampled all threads 4/4': 40791.0 > 0.0
FINE: isAsserted false, 'sharedDiff < 0.1': 0.0 < 0.1

Sample assertion failure:

java.lang.AssertionError: Expected 0.002 > 0.01: vtable stub > 0.01
at one.profiler.test.Assert.isAsserted(Assert.java:45)
at one.profiler.test.Assert.isGreater(Assert.java:54)
at test.recovery.RecoveryTests.numbers(RecoveryTests.java:42)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at one.profiler.test.Runner.run(Runner.java:151)
at one.profiler.test.Runner.run(Runner.java:167)
at one.profiler.test.Runner.main(Runner.java:216)

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Copy link
Member

@apangin apangin left a comment

Choose a reason for hiding this comment

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

Thanks, I like the idea. Please find my comments inline.


enum Comparison {
GT, GTE, LT, LTE,
}
Copy link
Member

Choose a reason for hiding this comment

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

Only one top-level class per file, please.
Make it a nested class or move to a separate file.

public class Assert {
private static final Logger log = Logger.getLogger(Assert.class.getName());
private static final Map<Comparison, String> operator = new HashMap<>();
private static final Map<Comparison, BiPredicate<Double, Double>> comparator = new HashMap<>();
Copy link
Member

Choose a reason for hiding this comment

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

No need for maps - just store them directly in enum's fields.
Alternatively, enum may itself implement BiPredicate.

Assert.isGreater(eventsCount.get("jdk.ExecutionSample"), 50);
Assert.isGreater(eventsCount.get("jdk.JavaMonitorEnter"), 50);
Assert.isGreater(eventsCount.get("jdk.ObjectAllocationInNewTLAB"), 50);
Assert.isGreater(eventsCount.get("jdk.ExecutionSample"), 50, "jdk.ExecutionSample > 50");
Copy link
Member

Choose a reason for hiding this comment

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

I'm not a fan of repeating assertion in a message every time - this is more of a boilerplate that does not add much value. It's ok to leave it without a message. Or, given that stacktrace already contains line numbers, we may extract a line from the source code and show it to a user. Not sure it's worth doing, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added source parser, assertion failures looks like this now:

java.lang.AssertionError: Expected 0.13666666666666666 < 0.01
at test/test/recovery/RecoveryTests.java:42
:41    Output out = p.profile("-d 3 -e cpu -o collapsed");
:42 👉 Assert.isLess(out.ratio("vtable stub"), 0.01);
:43    Assert.isGreater(out.ratio("Numbers.loop"), 0.8);
at one.profiler.test.Assert.assertComparison(Assert.java:44)
at one.profiler.test.Assert.isLess(Assert.java:81)
at test.recovery.RecoveryTests.numbers(RecoveryTests.java:42)

before:

java.lang.AssertionError: Expected 0.13666666666666666 < 0.01
at one.profiler.test.Assert.assertComparison(Assert.java:44)
at one.profiler.test.Assert.isLess(Assert.java:81)
at test.recovery.RecoveryTests.numbers(RecoveryTests.java:42)



enum Comparison {
GT, GTE, LT, LTE,
Copy link
Member

Choose a reason for hiding this comment

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

Add EQ for completeness?

@@ -0,0 +1,68 @@
package one.profiler.test;
Copy link
Member

Choose a reason for hiding this comment

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

We badly need a linter for file headers.

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we merge #1016 if I remove the trailing whitespace checker for now(it is scanning .class files and giving false results)

String className = element.getClassName();

try {
Class<?> clazz = Class.forName(className);
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need to instantiate a class just to convert its name to a path? Isn't className sufficient?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We already had fileName with extension, simplifying this.

}

private static String getFilePathFromClassLoader(String fileName, Class<?> clazz) {
String packagePath = clazz.getPackage().getName().replace('.', File.separatorChar);
Copy link
Member

Choose a reason for hiding this comment

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

I'd just use / for better readability. It works even on Windows.

Copy link
Member

Choose a reason for hiding this comment

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

It also makes sense to cut everything after the last $ in case it's a nested class.

return "Error reading source file: " + ex.getMessage();
}

return sourceCode.toString().trim();
Copy link
Member

Choose a reason for hiding this comment

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

Each line is already trimmed, isn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was to remove the last \n.

int startLine = Math.max(1, lineNumber - 1);
int endLine = lineNumber + 1;

// Read lines, and append the target lines (-1, 0, +1)
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need +1 line for assertions? The next assertion will not be executed.

Copy link
Member

Choose a reason for hiding this comment

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

Agree - I'll just leave one line with the original assertion for simplicity. We're not going to replace IDE 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.

In case the assertion spans multiple lines. Will simplify to single line then.

private static final Logger log = Logger.getLogger(Assert.class.getName());

private static void assertComparison(Comparison comparison, double left, double right, String message) {
boolean asserted = !comparison.comparator.test(left, right);
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't assertionFailed going to be a better name?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It didn't "fail" though, we are checking a value and we find it is "asserted". Here used as "asserted" vs. "unasserted". Can clarify further if necessary.

assert out.contains("sun/nio/ch/DatagramChannelImpl.send");
}
@Test(mainClass = DatagramTest.class, debugNonSafepoints = true) // Fails on Alpine
public void datagramSocketLock(TestProcess p) throws Exception {
Copy link
Member

Choose a reason for hiding this comment

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

What's happened with the indentation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixing.

out = p.profile("-e lock -d 3 -o collapsed");
assert out.contains("sun/nio/ch/DatagramChannelImpl.send");
}
@Test(mainClass = DatagramTest.class, debugNonSafepoints = true) // Fails on Alpine
Copy link
Member

Choose a reason for hiding this comment

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

Btw, the test no longer fails on Alpine, the comment can be removed.

sourceCode.append(filePath);
sourceCode.append(":");
sourceCode.append(lineNumber);
sourceCode.append("\n");
Copy link
Member

Choose a reason for hiding this comment

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

Since we return just one line, StringBuilder is redundant; use regular string concatenation or String.format.


while ((line = reader.readLine()) != null) {
if (currentLine == lineNumber) {
sourceCode.append(" 👉 ").append(line.trim());
Copy link
Member

Choose a reason for hiding this comment

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

Just return <string>;

}

private static String getFilePathFromClassLoader(String className) {
int dollar = className.lastIndexOf("$");
Copy link
Member

Choose a reason for hiding this comment

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

nit: certain string methods (including lastIndexOf) have single-char variants - prefer them where possible.

if (stackTrace.length > ignoreFrames) {
StackTraceElement element = stackTrace[ignoreFrames];
String className = element.getClassName();
String filePath = getFilePathFromClassLoader(className);
Copy link
Member

Choose a reason for hiding this comment

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

...FromClassName? ClassLoader is not involved here.

String operation = left + " " + comparison.operator + " " + right;

log.log(Level.FINE, "isAsserted " + asserted + ", "
+ (message == null ? "" : "'" + message + "'") + ": "
Copy link
Member

Choose a reason for hiding this comment

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

A string won't look right when message == null, will it?

Copy link
Member

Choose a reason for hiding this comment

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

Punctuation still looks wrong:

isAsserted false, : 0 < 1
                ^^^

Comment on lines 41 to 42
String lines = "\n" + SourceLocator.tryGetFrom(new Exception(), 2);
String msg = message != null ? (": " + message + "\n" + lines) : lines;
Copy link
Member

Choose a reason for hiding this comment

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

Construction is a bit hard to read. There'll be an extra empty line if the message is not empty, is it intended?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in message null and not-null cases, simplified the construct.

String operation = left + " " + comparison.operator + " " + right;

log.log(Level.FINE, "isAsserted " + asserted + ", "
+ (message == null ? "" : "'" + message + "'") + ": "
Copy link
Member

Choose a reason for hiding this comment

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

Punctuation still looks wrong:

isAsserted false, : 0 < 1
                ^^^


while ((line = reader.readLine()) != null) {
if (currentLine == lineNumber) {
return result + "\n\t👉 " + line.trim();
Copy link
Member

Choose a reason for hiding this comment

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

Can you construct the entire string here? It will be easier to read comparing to when the first part of the message is somewhere else.
Also, let's avoid non-ASCII character. It does not make a big difference here, but who knows where it might go off.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it is a good idea to keep concerns separate, getting the source code line and building the assertion message are different responsibilities. Renamed methods and classes and further simplified Assert.java

@apangin apangin merged commit 62dca46 into async-profiler:master Oct 17, 2024
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants