Skip to content

Commit

Permalink
Reduce memory usage of checkClasspathCompatible (#2791)
Browse files Browse the repository at this point in the history
Reduce memory usage of `checkClasspathCompatible`
  • Loading branch information
CRogers authored May 1, 2024
1 parent 0004d07 commit c482c93
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 34 deletions.
5 changes: 5 additions & 0 deletions changelog/@unreleased/pr-2791.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type: fix
fix:
description: Reduce memory usage of `checkClasspathCompatible`
links:
- https://github.com/palantir/gradle-baseline/pull/2791
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,18 @@

package com.palantir.baseline.plugins.javaversions;

import java.io.DataInputStream;
import com.google.common.io.ByteStreams;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Shorts;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Optional;
import java.util.jar.JarFile;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.provider.Property;
Expand Down Expand Up @@ -71,47 +75,51 @@ public final void action() {
}

private Optional<String> tooHighBytecodeMajorVersionInJar(File file) {
try (JarFile jarFile = new JarFile(file)) {
return jarFile.stream()
.flatMap(entry -> {
// We don't care about higher versions of classes in multi-release jars as JVMs will only
// load classes from here that match or are higher than their current version
boolean isMultiReleaseClass = entry.getName().contains("META-INF/versions");
boolean isntClassFile = !entry.getName().endsWith(".class");

if (isMultiReleaseClass || isntClassFile) {
return Stream.empty();
}

try (InputStream inputStream = jarFile.getInputStream(entry)) {
return bytecodeMajorVersionForClassFile(inputStream)
.filter(bytecodeMajorVersion -> bytecodeMajorVersion
> getJavaVersion().get().asBytecodeMajorVersion())
.map(bytecodeMajorVersion ->
entry.getName() + " has bytecode major version " + bytecodeMajorVersion)
.stream();
} catch (IOException e) {
throw new RuntimeException(
"Failed when checking classpath compatibility of " + file + ", class "
+ entry.getName(),
e);
}
})
.findFirst();
try (JarInputStream jarInputStream = new JarInputStream(new BufferedInputStream(new FileInputStream(file)))) {
JarEntry entry;
while ((entry = jarInputStream.getNextJarEntry()) != null) {
String entryName = entry.getName();
// We don't care about higher versions of classes in multi-release jars as JVMs will only
// load classes from here that match or are higher than their current version
boolean isMultiReleaseClass = entryName.contains("META-INF/versions");
boolean isntClassFile = !entryName.endsWith(".class");

if (isMultiReleaseClass || isntClassFile) {
continue;
}

Optional<String> bytecodeMajorVersionTooHigh = bytecodeMajorVersionForClassFile(jarInputStream)
.filter(bytecodeMajorVersion ->
bytecodeMajorVersion > getJavaVersion().get().asBytecodeMajorVersion())
.map(bytecodeMajorVersion -> entryName + " has bytecode major version " + bytecodeMajorVersion);

if (bytecodeMajorVersionTooHigh.isPresent()) {
return bytecodeMajorVersionTooHigh;
}
}
} catch (IOException e) {
throw new RuntimeException("Failed when checking classpath compatibility of: " + file, e);
}

return Optional.empty();
}

private static Optional<Integer> bytecodeMajorVersionForClassFile(InputStream classFile) throws IOException {
DataInputStream dataInputStream = new DataInputStream(classFile);
int magic = dataInputStream.readInt();
// Avoid DataInputStream as it allocates 240+ bytes on construction
byte[] buf = new byte[4];
ByteStreams.readFully(classFile, buf);
int magic = Ints.fromByteArray(buf);

if (magic != BYTECODE_IDENTIFIER) {
// Skip as it's not a class file
return Optional.empty();
}
int minorBytecodeVersion = 0xFFFF & dataInputStream.readShort();
int majorBytecodeVersion = 0xFFFF & dataInputStream.readShort();

// Read the minor and major version (both u16s)
ByteStreams.readFully(classFile, buf);

// The first two bytes make up the minor version, so take the second
int majorBytecodeVersion = 0xFFFF & Shorts.fromBytes(buf[2], buf[3]);

return Optional.of(majorBytecodeVersion);
}
Expand Down

0 comments on commit c482c93

Please sign in to comment.