Inspired by this SO post <https://stackoverflow.com/a/72437386/1278899> and
with some help from Piotr <https://stackoverflow.com/a/72916795/1278899>, I
have drafted an example where I redefine a class such that every logger
call is preceded with a static source location capture. The experiment aims
to replace the current source location capture that uses an exception-based
expensive mechanism with inlining the source location using bytecode
weaving. The sources are publicly available on GitHub.
<https://github.com/vy/asm-playground/tree/master/src/main/java/com/vlkan>
In a nutshell, the magic is as follows:

I have a logger library (Log4j.java
<https://github.com/vy/asm-playground/blob/master/src/main/java/com/vlkan/Log4j.java>)
as follows:

    public static final ThreadLocal<SourceLocation> LOCATION_REF =
ThreadLocal.withInitial(SourceLocation::new);

    public static void log() {
        SourceLocation location = LOCATION_REF.get();
        boolean locationProvided = location.lineNumber > 0;
        if (!locationProvided) {
            StackTraceElement[] stackTraceElements = new
Throwable().getStackTrace();
            // Skip the first element pointing to this method.
            StackTraceElement stackTraceElement = stackTraceElements[1];
            location.init(
                    stackTraceElement.getFileName(),
                    stackTraceElement.getClassName(),
                    stackTraceElement.getMethodName(),
                    stackTraceElement.getLineNumber());
        }
        System.out.format(
                "[%s] %s%n",
                location,
                locationProvided ? "provided location" : "populated
location");
    }

Here note how `log()` uses a thread-local to see if there is already a
`SourceLocation` provided. If so, it leverages that, otherwise it populates
the source location using the stack trace of an exception.

Below is my actual application (AppActual.java
<https://github.com/vy/asm-playground/blob/master/src/main/java/com/vlkan/AppActual.java>),
that is, what the actual/existing user code looks like:

    public static void main(String[] args) {
        System.out.println("should log at line 9");
        log();
        System.out.println("nothing to see here");
        System.out.println("should log at line 12");
        log();
        f();
    }

    private static void f() {
        System.out.println("adding some indirection");
        System.out.println("should log at line 19");
        log();
    }

I want to transform this into the following expected form (AppExpected.java
<https://github.com/vy/asm-playground/blob/master/src/main/java/com/vlkan/AppExpected.java>)
that exploits the `LOCATION_REF` thread-local to inline the source location
information:

    public static void main(String[] args) {
        System.out.println("should log at line 9");
        LOCATION_REF.get().init("AppExpected.java",
"com.vlkan.AppExpected", "main", 9);
        log();
        System.out.println("nothing to see here");
        System.out.println("should log at line 12");
        LOCATION_REF.get().init("AppExpected.java",
"com.vlkan.AppExpected", "main", 12);
        log();
        f();
    }

    private static void f() {
        System.out.println("adding some indirection");
        System.out.println("should log at line 19");
        LOCATION_REF.get().init("AppExpected.java",
"com.vlkan.AppExpected", "main", 19);
        log();
    }

And... 👉 AppTransforming.java
<https://github.com/vy/asm-playground/blob/master/src/main/java/com/vlkan/AppTransforming.java>
👈, my dear friends, performs exactly this transformation: converts
`AppActual` bytecode into `AppExpected`. 😍

I think we can extend this experiment to implement zero-cost source
location capture for Log4j. Though I will appreciate your help on some
loose ends. Assuming we have a bullet-proof mechanism to inline source
location capture given a class, what is the right way to ship this? As a
Maven plugin that kicks in at compile time? Wouldn't that make this feature
impossible to use without recompiling user sources? As a runtime utility?
If so, what about the cost of classpath scanning & weaving? If the bytecode
weaving only intercepts at Log4j API calls, this won't work for Log4j 1
bridge, SLF4J, or any other indirect access to the Log4j API. What do you
think? I have used a thread-local to pass the source location to the
caller, is there a better alternative? (Putting aside the dynamic-scoped
variables to be shipped with Loom.)

Reply via email to