diff --git a/build.gradle b/build.gradle index 4a10ff9682..ebf8eb5d1d 100644 --- a/build.gradle +++ b/build.gradle @@ -296,6 +296,7 @@ dependencies { implementation libs.asm // ByteCode manipulation implementation libs.asm.util // ASM utilities implementation libs.icu4j // Unicode support + implementation libs.jol.core // JVM object layout and graph footprint estimates implementation libs.jsoup // HTML5 parsing for HTML::Content::Extractor implementation libs.jing // RELAX NG validation for XML::LibXML implementation libs.snakeyaml.engine // YAML processing @@ -473,6 +474,8 @@ shadowJar { destinationDirectory = file("$buildDir/../target") manifest { attributes 'Main-Class': 'org.perlonjava.app.cli.Main' + attributes 'Premain-Class': 'org.openjdk.jol.vm.InstrumentationSupport' + attributes 'Launcher-Agent': 'org.openjdk.jol.vm.InstrumentationSupport' } exclude 'module-info.class' exclude 'META-INF/MANIFEST.MF' diff --git a/docs/about/changelog.md b/docs/about/changelog.md index cabaec64ed..989cda2b9c 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -6,6 +6,8 @@ priorities and future plans. ## Work in progress +- Bundle `Devel::Size` with JVM-based `size` and `total_size` estimates. + - Accept `experimental::for_list` as Perl 5.44's no-op warning category, align bundled warning masks including `experimental::builtin`, and expose B start-op package identity for Sub::Import compatibility (#1208, #1219, #1206). - Preserve the input line number when a borrowed anonymous filehandle alias leaves a JVM subroutine scope (#1933). diff --git a/docs/reference/bundled-modules.md b/docs/reference/bundled-modules.md index 8feb3ad338..453b9c3f72 100644 --- a/docs/reference/bundled-modules.md +++ b/docs/reference/bundled-modules.md @@ -23,6 +23,7 @@ Recent CPAN compatibility additions include: | `PadWalker` | Perl facade over Java runtime metadata | `peek_sub`, `closed_over`, and `set_closed_over`; caller-pad APIs remain unsupported | | `Devel::Caller` | Perl facade over Java call frames | `caller_cv` and caller argument compatibility used by lexical tooling | | `Devel::LexAlias` | Perl facade over Java lexical cells | Rebinds local and captured scalar, array, and hash lexicals | +| `Devel::Size` | Perl wrapper plus JOL | Estimates shallow JVM object size and reachable object graph size; totals include PerlOnJava runtime objects, and `$warn`/`$dangle` are compatibility variables only | | `Encode::Locale` | Pure Perl facade over bundled locale support | Provides LWP and XML::Parser with locale encoding aliases; includes `Encode::Alias` | | `Crypt::Twofish2` | Java XS bridge | BouncyCastle Twofish engine with ECB, CBC, and CFB1 compatibility | | `Crypt::Blowfish` | Java XS bridge | BouncyCastle Blowfish engine used by `Crypt::CBC` and `Git::Crypt` | diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 70675ff7ec..56cae81fc9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,7 @@ icu4j = "78.3" jsoup = "1.23.1" jing = "20241231" junit-jupiter = "6.1.3" +jol = "0.17" snakeyaml-engine = "3.1.1" snappy-java = "1.1.10.8" sqlite-jdbc = "3.53.2.1" @@ -33,6 +34,7 @@ jing = { module = "org.relaxng:jing", version.ref = "jing" } junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit-jupiter" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit-jupiter" } junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit-jupiter" } +jol-core = { module = "org.openjdk.jol:jol-core", version.ref = "jol" } snakeyaml-engine = { module = "org.snakeyaml:snakeyaml-engine", version.ref = "snakeyaml-engine" } snappy-java = { module = "org.xerial.snappy:snappy-java", version.ref = "snappy-java" } sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite-jdbc" } diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/DevelSize.java b/src/main/java/org/perlonjava/runtime/perlmodule/DevelSize.java new file mode 100644 index 0000000000..ded4521f27 --- /dev/null +++ b/src/main/java/org/perlonjava/runtime/perlmodule/DevelSize.java @@ -0,0 +1,57 @@ +package org.perlonjava.runtime.perlmodule; + +import org.openjdk.jol.info.ClassLayout; +import org.openjdk.jol.info.GraphLayout; +import org.perlonjava.runtime.runtimetypes.RuntimeArray; +import org.perlonjava.runtime.runtimetypes.RuntimeList; +import org.perlonjava.runtime.runtimetypes.RuntimeScalar; + +/** JVM object footprint estimates exposed by Devel::Size. */ +public class DevelSize extends PerlModuleBase { + + static { + // The launcher may not permit dynamic agent attachment. JOL's Unsafe + // layout strategy still works without it; avoid warning on every call. + System.setProperty("jol.skipInstallAttach", "true"); + System.setProperty("jol.skipDynamicAttach", "true"); + } + + public DevelSize() { + super("Devel::Size", false); + } + + public static void initialize() { + DevelSize module = new DevelSize(); + try { + module.registerMethod("size", null); + module.registerMethod("total_size", null); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + module.defineExport("EXPORT_OK", "size", "total_size"); + module.defineDefaultAndAllTags(); + } + + /** Return the shallow JVM size of the Perl scalar and its inline payload. */ + public static RuntimeList size(RuntimeArray args, int ctx) { + if (args == null || args.isEmpty() || args.get(0) == null) { + return new RuntimeScalar(0L).getList(); + } + RuntimeScalar scalar = args.get(0); + long bytes = ClassLayout.parseInstance(scalar).instanceSize(); + if (!org.perlonjava.runtime.runtimetypes.RuntimeScalarType.isReference(scalar) + && scalar.value != null) { + bytes += GraphLayout.parseInstance(scalar.value).totalSize(); + } + return new RuntimeScalar(bytes).getList(); + } + + /** Return the JVM footprint of all objects reachable from the scalar. */ + public static RuntimeList total_size(RuntimeArray args, int ctx) { + if (args == null || args.isEmpty() || args.get(0) == null) { + return new RuntimeScalar(0L).getList(); + } + long bytes = GraphLayout.parseInstance(args.get(0)).totalSize(); + return new RuntimeScalar(bytes).getList(); + } +} diff --git a/src/main/perl/lib/Devel/Size.pm b/src/main/perl/lib/Devel/Size.pm new file mode 100644 index 0000000000..16275a0bbd --- /dev/null +++ b/src/main/perl/lib/Devel/Size.pm @@ -0,0 +1,21 @@ +package Devel::Size; + +use strict; +use warnings; +require Exporter; +our @ISA = qw(Exporter); +our $VERSION = '0.87'; +our @EXPORT_OK = qw(size total_size); +our %EXPORT_TAGS = (all => \@EXPORT_OK); + +# Retained for source compatibility. JOL can inspect the JVM graph directly, +# so native XS warnings about incomplete CVs and dangling C pointers do not +# apply; these variables do not change JOL's behavior. The estimate includes +# reachable PerlOnJava implementation objects. +our $warn = 1; +our $dangle = 0; + +require XSLoader; +XSLoader::load(__PACKAGE__, $VERSION); + +1; diff --git a/src/test/resources/module/Devel-Size/t/basic.t b/src/test/resources/module/Devel-Size/t/basic.t new file mode 100644 index 0000000000..3bef5009c6 --- /dev/null +++ b/src/test/resources/module/Devel-Size/t/basic.t @@ -0,0 +1,34 @@ +use strict; +use warnings; +use Test::More; + +use Devel::Size qw(size total_size); + +ok(size(1) > 0, 'size returns a positive estimate'); +ok(total_size(1) >= size(1), 'total_size includes the root object'); +ok(size('a much longer string value') > size('x'), + 'size accounts for scalar string payload'); + +my $small = [1]; +my $large = [1 .. 100]; +ok(total_size($large) > total_size($small), 'aggregate size grows with contents'); + +my $small_hash = { value => 'x' }; +my $large_hash = { value => 'x' x 1000 }; +ok(total_size($large_hash) > total_size($small_hash), + 'hash size grows with contained data'); + +my $shared = [1 .. 20]; +my $twice = [$shared, $shared]; +my $once = [$shared]; +ok(total_size($twice) < 2 * total_size($once), 'shared referent is deduplicated'); + +my $cycle = []; +push @$cycle, $cycle; +ok(total_size($cycle) > 0, 'cyclic graph terminates'); + +use Devel::Size qw(:all); +ok(defined &Devel::Size::size && defined &Devel::Size::total_size, + ':all exposes both functions'); + +done_testing();