You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: AGENT_ARCH.md
+14-14Lines changed: 14 additions & 14 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,6 +1,6 @@
1
-
# LazyValGrade Agent Architecture: A Catalog of Weird Shit
1
+
# Sloth Agent Architecture: A Catalog of Weird Shit
2
2
3
-
This document describes the non-obvious design decisions, workarounds, and architectural quirks in the LazyValGrade java agent. If you're reading this, you're either maintaining this codebase or morbidly curious about what it takes to rewrite bytecode at class-load time inside a running JVM.
3
+
This document describes the non-obvious design decisions, workarounds, and architectural quirks in the Sloth java agent. If you're reading this, you're either maintaining this codebase or morbidly curious about what it takes to rewrite bytecode at class-load time inside a running JVM.
4
4
5
5
## Table of Contents
6
6
@@ -34,7 +34,7 @@ The agent's own dependencies -- including the Scala standard library -- are comp
34
34
35
35
The pipeline:
36
36
1. Build the CLI jar (which contains the patcher)
37
-
2. Run the CLI patcher against each dependency JAR: `java -cp <cliJar + deps> lazyvalgrade.cli.Main <depJar>`
37
+
2. Run the CLI patcher against each dependency JAR: `java -cp <cliJar + deps> sloth.cli.Main <depJar>`
38
38
3. Swap the patched JARs into the agent's assembly classpath
39
39
40
40
This is the `processDeps` task. The agent literally patches itself with itself.
@@ -47,9 +47,9 @@ The `DEBUG_AGENT_ASSEMBLY` environment variable enables verbose logging of this
sbt-assembly shade rules rewrite all `scala.**` references to `lazyvalgrade.shaded.scala.**`. But the patcher needs to match and generate references to the *application's* unshaded `scala.runtime.LazyVals$`, not the agent's own shaded copy.
52
+
sbt-assembly shade rules rewrite all `scala.**` references to `sloth.shaded.scala.**`. But the patcher needs to match and generate references to the *application's* unshaded `scala.runtime.LazyVals$`, not the agent's own shaded copy.
53
53
54
54
Two different workarounds are used, for two different reasons:
55
55
@@ -89,7 +89,7 @@ The `isAssignableFrom` method reimplements the full interface-and-superclass che
Companion pairs (`Foo$` and `Foo`) must be patched together because OFFSET fields can live in the companion class while the `lzyINIT` methods live in the companion object. But the JVM's `ClassFileTransformer` only provides one class at a time.
95
95
@@ -109,7 +109,7 @@ The solution uses a `ConcurrentHashMap` as a cross-load buffer:
A `ClassFileTransformer` must never throw -- returning `null` means "leave unchanged." The catch block catches `Throwable` and returns `null`.
115
115
@@ -119,15 +119,15 @@ A `ClassFileTransformer` must never throw -- returning `null` means "leave uncha
119
119
120
120
## 6. Double-Attach Guard
121
121
122
-
**File:**`LazyValGradeAgent.scala` (lines 20-29)
122
+
**File:**`SlothAgent.scala` (lines 20-29)
123
123
124
124
An `AtomicInteger` detects if `premain` is called more than once, which happens if `-javaagent` is specified alongside `JAVA_TOOL_OPTIONS` containing the same agent. On the second call, it throws a `RuntimeException` to prevent duplicate transformers that would double-patch classfiles. The error message specifically tells the user to remove one of the two agent specifications.
Before doing any real work, the transformer runs a lightweight ASM pass with `SKIP_CODE | SKIP_DEBUG` that only visits field declarations. It short-circuits as soon as it finds either:
133
133
- A `$lzy` field (but NOT `$lzyHandle` -- that indicates 3.8+ format, already good)
@@ -139,7 +139,7 @@ This avoids the cost of full parsing, companion loading, and group analysis for
139
139
140
140
## 8. getResourceAsStream: Pure I/O, Not Class Loading
The companion's bytes are loaded via `loader.getResourceAsStream(name + ".class")` rather than `Class.forName` or `loadClass`. This is explicitly noted as "pure I/O, no class definition" because `loadClass` would:
145
145
1. Trigger the transformer recursively
@@ -257,9 +257,9 @@ The agent shades *all* of its dependencies and itself:
-`com.lihaoyi.**`, `os.**`, `geny.**` -- os-lib and its dependencies
260
-
-`lazyvalgrade.**` -- the entire lazyvalgrade itself
260
+
-`sloth.**` -- the entire sloth itself
261
261
262
-
This is necessary because the agent runs in the application's JVM and cannot conflict with the application's own versions of these libraries. But it creates the anti-shading problems described in section 2, and is why the self-patching build (section 1) exists. Self-shading of the `lazyvalgrade.**` package is only really necessary for agent to be able to run in its own tests.
262
+
This is necessary because the agent runs in the application's JVM and cannot conflict with the application's own versions of these libraries. But it creates the anti-shading problems described in section 2, and is why the self-patching build (section 1) exists. Self-shading of the `sloth.**` package is only really necessary for agent to be able to run in its own tests.
263
263
264
264
---
265
265
@@ -281,9 +281,9 @@ This is why `sbt test` is slow and the CLAUDE.md instructions say to always use
At trace log level (`-javaagent:agent.jar=trace`), the transformer dumps every patched class's bytes to `/tmp/lazyvalgrade-dump-<pid>-<name>.class`. The filename sanitizes `$` and `.` to `_`. These dumps happen even on success and are purely for post-mortem `javap` inspection.
286
+
At trace log level (`-javaagent:agent.jar=trace`), the transformer dumps every patched class's bytes to `/tmp/sloth-dump-<pid>-<name>.class`. The filename sanitizes `$` and `.` to `_`. These dumps happen even on success and are purely for post-mortem `javap` inspection.
This covers any JVM process launched in that shell -- sbt, scala-cli, Mill, Gradle, plain `java`, etc. The agent automatically detects and patches Scala 3.0-3.7.x lazy val bytecode at class-load time.
118
118
119
119
For a single invocation without environment variables:
The CLI recursively finds all `.class` files in the given directory, detects Scala 3.0-3.7.x lazy val implementations, and rewrites them to the 3.8+ VarHandle-based format. Use this for producing patched artifacts in build pipelines (assembly JARs, Docker images, etc.).
@@ -156,7 +156,7 @@ The CLI recursively finds all `.class` files in the given directory, detects Sca
The agent intercepts class loading, detects Scala 3.0-3.7.x lazy val bytecode, and rewrites it to the 3.8+ format before the class is loaded. No changes to application code or build required.
0 commit comments