Skip to content

Track start/end template + before/after node resolve #48693

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

angelozerr
Copy link

@angelozerr angelozerr commented Jun 30, 2025

This PR provides the capability to track start/end template rendering and before/after node resolve (it follows the same idea than Saxon events used to implement a debugger). I need this support to provide the Qute debugger that I am implementing and integrating in IntelliJ (and it will be easy to integrate in vscode and even in Eclipse IDE):

Here a demo with breakpoint and steps:

QuteDebuggerDemo

Here a demo with expression eval:

QuteDebuggerDemoEval2

Breakpoint with condition is supported:

image

image

Engine#addTraceListener(TraceListener listener)

The track can be done by adding a TraceListener registered with Engine#addTraceListener(TraceListener listener)

Here a sample how to use TraceListener:

import io.quarkus.qute.Engine;
import io.quarkus.qute.ReflectionValueResolver;
import io.quarkus.qute.Template;
import io.quarkus.qute.trace.ResolveEvent;
import io.quarkus.qute.trace.TemplateEvent;
import io.quarkus.qute.trace.TraceListener;

public class TraceSample {

    public static void main(String[] args) throws InterruptedException {

        Engine engine = Engine.builder()
                .addDefaults()
                .addValueResolver(new ReflectionValueResolver()).build();

        Template template = engine.parse("""
                <html>
                   Hello {name}!
                </html>
                """);

        engine.addTraceListener(new TraceListener() {
            @Override
            public void onStartTemplate(TemplateEvent event) {
                System.err.println("Starting template (id): " + event.getTemplateInstance().getTemplate().getId());
            }

            @Override
            public void onBeforeResolve(ResolveEvent event) {
                System.err.println("Before node resolve: " + event.getTemplateNode());
            }

            @Override
            public void onAfterResolve(ResolveEvent event) {
                System.err.println("After node resolve: " + event.getTemplateNode() + " in " + event.getEllapsedTime() + "ms");
            }

            @Override
            public void onEndTemplate(TemplateEvent event) {
                System.err.println("Rendering template (id): " + event.getTemplateInstance().getTemplate().getId() + " in " + event.getEllapsedTime() + "ms");
            }
        });


        String s = template.data("name", "Quarkus")
                .render();
        System.err.println(s);

    }
}

You will see in the console the following trace:

Starting template (id): 1
Before node resolve: TextNode [value=<html>
   Hello ]
After node resolve: TextNode [value=<html>
   Hello ] in 1ms
Before node resolve: ExpressionNode [expression=Expression [namespace=null, parts=[name], literal=null]]
After node resolve: ExpressionNode [expression=Expression [namespace=null, parts=[name], literal=null]] in 8ms
Before node resolve: TextNode [value=!
</html>
]
After node resolve: TextNode [value=!
</html>
] in 0ms
Rendering template (id): 1 in 23ms
<html>
   Hello Quarkus!
</html>

Register TraceListener with Java SPI

As you can see the PR have no dependencies to the Qute debugger. How Qute debugger can be registered?

The answer is that Qute debugger will track creation of Engine instance with the existing EngineListener API. Qute debugger need to register a custom EngineListener (RegisterDebugServerAdapter) to track Engine build. To do that the Qute debugger register this custom EngineListener with Java SPI with the following file stored in the Qute debugger artifact

src/main/resources/META-INF/services/io.quarkus.qute.EngineBuilder$EngineListener which contains io.quarkus.qute.debug.adapter.RegisterDebugServerAdapter

The Qute debugger adds a custom TraceListener to block the process (by using wait() in onAfterResolve if there is a breakpoint defined for the resolved node.

@angelozerr angelozerr force-pushed the qute_trace branch 6 times, most recently from cf41f2c to 1768462 Compare June 30, 2025 13:12
@angelozerr angelozerr marked this pull request as ready for review June 30, 2025 13:30
@angelozerr
Copy link
Author

@mkouba @maxandersen @ia3andy please review this PR because I need it to provide Qute debugger which starts working.

@angelozerr
Copy link
Author

@FroMage please also review this PR

@ia3andy
Copy link
Contributor

ia3andy commented Jun 30, 2025

That looks very cool, as this is based on the contract that will be hard to change, we need to make sure the api/contract is flexible and will not change in the future.

@angelozerr
Copy link
Author

That looks very cool, as this is based on the contract that will be hard to change, we need to make sure the api/contract is flexible and will not change in the future.

Perhaps I will need more changes because for the moment my Qute debugger is just a POC and I have hard coded some things like src/main/resources/templates folder because I need to retrieve file path from a given template id since Qute works only with template id and not with file path.

But I think the API to track nodes is enough.

Copy link
Member

@FroMage FroMage left a comment

Choose a reason for hiding this comment

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

LGTM, though I'm not a fan of the nested interface for the service file, we should probably move it out.
But I'll let @mkouba merge this.

@@ -58,6 +59,11 @@ public final class EngineBuilder {
this.timeout = 10_000;
this.useAsyncTimeout = true;
this.listeners = new ArrayList<>();
// Load EngineListener implementations via Java SPI and register them.
ServiceLoader<EngineListener> listenerExtensions = ServiceLoader.load(EngineListener.class);
Copy link
Member

Choose a reason for hiding this comment

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

Apparently this is an inner class, making it hard to add a services file for it, because EngineBuilder$EngineListener is often an annoying character to deal with from the command-line.

Copy link
Author

Choose a reason for hiding this comment

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

I know but I don't want to change the existing API.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we use the service loader at all? I mean, we don't use service loaders for anything else. The EngineBuilder is intended to be used purely programmatically.

Copy link
Author

Choose a reason for hiding this comment

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

I need this service loader for the Qute debuger. The Qute debugger need to track all Engine which was built. If your Quarkus App uses for instance N EngineBuilder, the Qute debugger can track all engine created by the N EngineBuilder.

By using ServiceLoader, the Qute core have no dependencies to the Qute debugger.

private static CompletionStage<ResultNode> resolveWith(TemplateNode templateNode, ResolutionContext context) {
private static CompletionStage<ResultNode> resolveWith(TemplateNode templateNode, ResolutionContext context,
Engine engine) {
if (!engine.hasTraceListeners()) {
Copy link
Member

Choose a reason for hiding this comment

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

Oh that's where it's used, OK.

@angelozerr
Copy link
Author

LGTM, though I'm not a fan of the nested interface for the service file, we should probably move it out. But I'll let @mkouba merge this.

I agree with you but it is the existing API.

@angelozerr angelozerr marked this pull request as draft June 30, 2025 16:21
@angelozerr
Copy link
Author

@mkouba I switched the PR to draft because I have some bugs with item inside #for while debugging.

I don't know if the problem comes from this PR.

@angelozerr angelozerr marked this pull request as ready for review July 1, 2025 09:28
@angelozerr
Copy link
Author

@mkouba sorry for the noise, the bug was inside my current Qute debugger, this PR works perfectly for now.

@Override
public TraceManager getTraceManager() {
if (traceManager == null) {
traceManager = new TraceManager();
Copy link
Contributor

Choose a reason for hiding this comment

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

Why the lazy loading here? Also you would need use the volatile modifier + synchronize, or the io.quarkus.qute.LazyValue abstraction to make it thread-safe.

Copy link
Author

Choose a reason for hiding this comment

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

I wanted to create a trace manager if nobody regsiters a listener, but I agree with you, I can create it every time and set it as final, since it exists hasTraceListeners.

*/
public class TraceManager {

private List<TraceListener> listeners;
Copy link
Contributor

Choose a reason for hiding this comment

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

You should use a thread-safe List variant (such as CopyOnWriteArrayList). And ideally, the reference should be final.

Copy link
Author

Choose a reason for hiding this comment

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

Indeed it is very basic, I need to improve that but for the moment I have not seen some trouble.

Signed-off-by: azerr <azerr@redhat.com>
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.

4 participants