/* * Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package build.tools.jigsaw; import java.io.IOException; import java.io.PrintStream; import java.lang.module.Configuration; import java.lang.module.ModuleDescriptor; import java.lang.module.ModuleFinder; import java.lang.module.ModuleReference; import java.lang.module.ResolvedModule; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.ArrayList; import java.util.Comparator; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import static java.lang.module.ModuleDescriptor.*; import static build.tools.jigsaw.ModuleSummary.HtmlDocument.Selector.*; import static build.tools.jigsaw.ModuleSummary.HtmlDocument.Division.*; public class ModuleSummary { private static final String USAGE = "Usage: ModuleSummary --module-path -o [--root mn]*"; public static void main(String[] args) throws Exception { int i=0; Path modpath = null; Path outfile = null; Set roots = new HashSet<>(); while (i < args.length && args[i].startsWith("-")) { String arg = args[i++]; switch (arg) { case "--module-path": modpath = Paths.get(args[i++]); break; case "-o": outfile = Paths.get(args[i++]); break; case "--root": roots.add(args[i++]); default: System.err.println(USAGE); System.exit(-1); } } if (outfile == null || modpath == null) { System.err.println(USAGE); System.exit(1); } Path dir = outfile.getParent() != null ? outfile.getParent() : Paths.get("."); Files.createDirectories(dir); Map modules = new HashMap<>(); Set mrefs = ModuleFinder.ofSystem().findAll(); for (ModuleReference mref : mrefs) { String mn = mref.descriptor().name(); Path jmod = modpath.resolve(mn + ".jmod"); modules.put(mn, new ModuleSummary(mref, jmod)); } if (roots.isEmpty()) { roots.addAll(modules.keySet()); } genReport(outfile, modules, roots, "JDK Module Summary"); } static void genReport(Path outfile, Map modules, Set roots, String title) throws IOException { Configuration cf = resolve(roots); try (PrintStream out = new PrintStream(Files.newOutputStream(outfile))) { HtmlDocument doc = new HtmlDocument(title, modules); Set descriptors = cf.modules().stream() .map(ResolvedModule::reference) .map(ModuleReference::descriptor) .collect(Collectors.toSet()); doc.writeTo(out, descriptors); } } private final String name; private final ModuleDescriptor descriptor; private final JmodInfo jmodInfo; ModuleSummary(ModuleReference mref, Path jmod) throws IOException { this.name = mref.descriptor().name(); this.descriptor = mref.descriptor(); this.jmodInfo = new JmodInfo(jmod); } String name() { return name; } long uncompressedSize() { return jmodInfo.size; } long jmodFileSize() { return jmodInfo.filesize; // estimated compressed size } ModuleDescriptor descriptor() { return descriptor; } int numClasses() { return jmodInfo.classCount; } long classBytes() { return jmodInfo.classBytes; } int numResources() { return jmodInfo.resourceCount; } long resourceBytes() { return jmodInfo.resourceBytes; } int numConfigs() { return jmodInfo.configCount; } long configBytes() { return jmodInfo.configBytes; } int numCommands() { return jmodInfo.nativeCmds.size(); } long commandBytes() { return jmodInfo.nativeCmds.values().stream() .mapToLong(l -> l.longValue()).sum() - jmodInfo.debugInfoCmdBytes; } int numCommandsDebug() { return jmodInfo.debugInfoCmdCount; } long commandDebugBytes() { return jmodInfo.debugInfoCmdBytes; } int numNativeLibraries() { return jmodInfo.nativeLibs.size(); } long nativeLibrariesBytes() { return jmodInfo.nativeLibs.values().stream() .mapToLong(l -> l.longValue()).sum() - jmodInfo.debugInfoLibBytes; } int numNativeLibrariesDebug() { return jmodInfo.debugInfoLibCount; } long nativeLibrariesDebugBytes() { return jmodInfo.debugInfoLibBytes; } Map commands() { return jmodInfo.nativeCmds; } Map nativeLibs() { return jmodInfo.nativeLibs; } Map configFiles() { return jmodInfo.configFiles; } static class JmodInfo { final long size; final long filesize; final int classCount; final long classBytes; final int resourceCount; final long resourceBytes; final int configCount; final long configBytes; final int debugInfoLibCount; final long debugInfoLibBytes; final int debugInfoCmdCount; final long debugInfoCmdBytes; final Map configFiles = new HashMap<>(); final Map nativeCmds = new HashMap<>(); final Map nativeLibs = new HashMap<>(); JmodInfo(Path jmod) throws IOException { long total = 0; long cBytes = 0, rBytes = 0, cfBytes = 0, dizLibBytes = 0, dizCmdBytes = 0; int cCount = 0, rCount = 0, cfCount = 0, dizLibCount = 0, dizCmdCount = 0; try (ZipFile zf = new ZipFile(jmod.toFile())) { for (Enumeration e = zf.entries(); e.hasMoreElements(); ) { ZipEntry ze = e.nextElement(); String fn = ze.getName(); int pos = fn.indexOf('/'); String dir = fn.substring(0, pos); String filename = fn.substring(fn.lastIndexOf('/') + 1); // name shown in the column String name = filename; long len = ze.getSize(); total += len; switch (dir) { case NATIVE_LIBS: nativeLibs.put(name, len); if (filename.endsWith(".diz")) { dizLibCount++; dizLibBytes += len; } break; case NATIVE_CMDS: nativeCmds.put(name, len); if (filename.endsWith(".diz")) { dizCmdCount++; dizCmdBytes += len; } break; case CLASSES: if (filename.endsWith(".class")) { cCount++; cBytes += len; } else { rCount++; rBytes += len; } break; case CONFIG: configFiles.put(name, len); cfCount++; cfBytes += len; break; default: break; } } this.filesize = jmod.toFile().length(); this.classCount = cCount; this.classBytes = cBytes; this.resourceCount = rCount; this.resourceBytes = rBytes; this.configCount = cfCount; this.configBytes = cfBytes; this.size = total; this.debugInfoLibCount = dizLibCount; this.debugInfoLibBytes = dizLibBytes; this.debugInfoCmdCount = dizCmdCount; this.debugInfoCmdBytes = dizCmdBytes; } } static final String NATIVE_LIBS = "native"; static final String NATIVE_CMDS = "bin"; static final String CLASSES = "classes"; static final String CONFIG = "conf"; static final String MODULE_ID = "module/id"; static final String MODULE_MAIN_CLASS = "module/main-class"; } static Configuration resolve(Set roots) { return Configuration.empty() .resolve(ModuleFinder.ofSystem(), ModuleFinder.of(), roots); } static class HtmlDocument { final String title; final Map modules; boolean requiresTransitiveNote = false; boolean aggregatorNote = false; boolean totalBytesNote = false; HtmlDocument(String title, Map modules) { this.title = title; this.modules = modules; } void writeTo(PrintStream out, Set selectedModules) { out.format("%n"); out.format("%s%n", title); // stylesheet Arrays.stream(HtmlDocument.STYLES).forEach(out::println); out.format("%n"); // body begins out.format("%n"); // title and date out.println(DOCTITLE.toString(title)); out.println(VERSION.toString(String.format("%tc", new Date()))); // total modules and sizes long totalBytes = selectedModules.stream() .map(ModuleDescriptor::name) .map(modules::get) .mapToLong(ModuleSummary::uncompressedSize) .sum(); String[] sections = new String[] { String.format("%s: %d", "Total modules", selectedModules.size()), String.format("%s: %,d bytes (%s %s)", "Total size", totalBytes, System.getProperty("os.name"), System.getProperty("os.arch")) }; out.println(SECTION.toString(sections)); // write table and header out.println(String.format("", MODULES)); out.println(header("Module", "Requires", "Exports", "Services", "Commands/Native Libraries/Configs")); // write contents - one row per module selectedModules.stream() .sorted(Comparator.comparing(ModuleDescriptor::name)) .map(m -> modules.get(m.name())) .map(ModuleTableRow::new) .forEach(table -> table.writeTo(out)); out.format("
"); // end table out.format(""); out.println(""); } String header(String... columns) { StringBuilder sb = new StringBuilder(); sb.append(""); Arrays.stream(columns) .forEach(cn -> sb.append(" ").append(cn).append("").append("\n")); sb.append(""); return sb.toString(); } static enum Selector { MODULES("modules"), MODULE("module"), MODULE_DEF("code name def"), AGGREGATOR("code name def agg"), REQUIRES("code"), REQUIRES_PUBLIC("code reexp"), BR("br"), CODE("code"), NUMBER("number"),; final String name; Selector(String name) { this.name = name; } @Override public String toString() { return name; } } static enum Division { DOCTITLE("doctitle"), VERSION("versions"), SECTION("section"); final String name; Division(String name) { this.name = name; } public String toString(String... lines) { String value = Arrays.stream(lines).collect(Collectors.joining("
\n")); return "
" + value + "
"; } } class ModuleTableRow { private final ModuleSummary ms; private final Set deps; private final int maxRows; private final boolean aggregator; ModuleTableRow(ModuleSummary ms) { this.ms = ms; Configuration cf = resolve(Set.of(ms.name())); this.deps = cf.modules().stream() .map(ResolvedModule::reference) .map(ModuleReference::descriptor) .collect(Collectors.toSet()); int count = (ms.numClasses() > 0 ? 1 : 0) + (ms.numResources() > 0 ? 1 : 0) + (ms.numConfigs() > 0 ? 1 : 0) + (ms.numNativeLibraries() > 0 ? 1 : 0) + (ms.numNativeLibrariesDebug() > 0 ? 1 : 0) + (ms.numCommands() > 0 ? 1 : 0) + (ms.numCommandsDebug() > 0 ? 1 : 0); this.aggregator = ms.numClasses() == 1 && count == 1; // only module-info.class // 5 fixed rows (name + 2 transitive count/size + 2 blank rows) this.maxRows = 5 + count + (aggregator && !aggregatorNote ? 2 : 0); } public void writeTo(PrintStream out) { out.println(String.format("", ms.name(), MODULE)); out.println(moduleColumn()); out.println(requiresColumn()); out.println(exportsColumn()); out.println(servicesColumn()); out.println(otherSectionColumn()); out.println(""); out.println(""); } public String moduleColumn() { // module name StringBuilder sb = new StringBuilder(" "); sb.append(""); sb.append(String.format("", MODULE)).append("\n"); sb.append(moduleName(ms.name())); sb.append(blankRow()); // metadata sb.append(toTableRow("class", "classes", ms.numClasses(), ms.classBytes())); sb.append(toTableRow("resource", "resources", ms.numResources(), ms.resourceBytes())); sb.append(toTableRow("config", "configs", ms.numConfigs(), ms.configBytes())); sb.append(toTableRow("native library", "native libraries", ms.numNativeLibraries(), ms.nativeLibrariesBytes())); sb.append(toTableRow("native library debug", "native libraries debug", ms.numNativeLibrariesDebug(), ms.nativeLibrariesDebugBytes())); sb.append(toTableRow("command", "commands", ms.numCommands(), ms.commandBytes())); sb.append(toTableRow("command debug", "commands debug", ms.numCommandsDebug(), ms.commandDebugBytes())); sb.append(blankRow()); // transitive dependencies long reqBytes = deps.stream() .filter(d -> !d.name().equals(ms.name())) .mapToLong(d -> modules.get(d.name()).uncompressedSize()) .sum(); long reqJmodFileSize = deps.stream() .mapToLong(d -> modules.get(d.name()).jmodFileSize()) .sum(); // size if (totalBytesNote) { sb.append(toTableRow("Total bytes", ms.uncompressedSize())); sb.append(toTableRow("Total bytes of dependencies", reqBytes)); } else { // print footnote sb.append(toTableRow("Total bytes1", ms.uncompressedSize())); sb.append(toTableRow("Total bytes of dependencies2", reqBytes)); } String files = deps.size() == 1 ? "file" : "files"; sb.append(toTableRow(String.format("Total jmod bytes (%d %s)", deps.size(), files), reqJmodFileSize)); if (aggregator && !aggregatorNote) { aggregatorNote = true; sb.append(blankRow()); sb.append(toTableRow("* aggregator is a module with module-info.class only", BR)); } if (!totalBytesNote) { totalBytesNote = true; sb.append(blankRow()); sb.append(toTableRow("1sum of all files including debug files", BR)); sb.append(toTableRow("2sum of direct and indirect dependencies", BR)); } sb.append("
").append(""); return sb.toString(); } private String moduleName(String mn) { if (aggregator) { StringBuilder sb = new StringBuilder(); sb.append(String.format("", AGGREGATOR)) .append(mn) .append("").append("  "); if (!aggregatorNote) { sb.append("(aggregator*)"); } else { sb.append("(aggregator)"); } sb.append(""); return sb.toString(); } else { return toTableRow(mn, MODULE_DEF); } } public String requiresColumn() { StringBuilder sb = new StringBuilder(); sb.append(String.format("")); boolean footnote = requiresTransitiveNote; ms.descriptor().requires().stream() .sorted(Comparator.comparing(Requires::name)) .forEach(r -> { boolean requiresTransitive = r.modifiers().contains(Requires.Modifier.TRANSITIVE); Selector sel = requiresTransitive ? REQUIRES_PUBLIC : REQUIRES; String req = String.format("%s", sel, r.name(), r.name()); if (!requiresTransitiveNote && requiresTransitive) { requiresTransitiveNote = true; req += "*"; } sb.append(req).append("\n").append("
"); }); if (!ms.name().equals("java.base")) { int directDeps = ms.descriptor().requires().size(); int indirectDeps = deps.size()-directDeps-1; for (int i=directDeps; i< (maxRows-1); i++) { sb.append("
"); } sb.append("
"); sb.append("+").append(indirectDeps).append(" transitive dependencies"); } if (footnote != requiresTransitiveNote) { sb.append("

").append("* bold denotes requires transitive"); } sb.append(""); return sb.toString(); } public String exportsColumn() { StringBuilder sb = new StringBuilder(); sb.append(String.format(" ", CODE)); ms.descriptor().exports().stream() .sorted(Comparator.comparing(Exports::source)) .filter(e -> !e.isQualified()) .forEach(e -> sb.append(e.source()).append("
").append("\n")); sb.append(""); return sb.toString(); } private String providesEntry(Provides p) { StringBuilder sb = new StringBuilder(); sb.append(String.format("provides %s
\n", p.service())); List pvs = new ArrayList<>(p.providers()); pvs.sort(Comparator.naturalOrder()); for (int i = 0; i < pvs.size(); i++) { // My kingdom for Stream::zip ... String fmt = ((i == 0) ? "    with %s" : ",
         %s"); sb.append(String.format(fmt, pvs.get(i))); } sb.append("\n"); return sb.toString(); } public String servicesColumn() { StringBuilder sb = new StringBuilder(); sb.append(String.format(" ", CODE)); ms.descriptor().uses().stream() .sorted() .forEach(s -> sb.append("uses ").append(s).append("
").append("\n")); ms.descriptor().provides().stream() .sorted(Comparator.comparing(Provides::service)) .map(this::providesEntry) .forEach(p -> sb.append(p).append("
").append("\n")); sb.append(""); return sb.toString(); } public String otherSectionColumn() { StringBuilder sb = new StringBuilder(); sb.append(""); sb.append(String.format("", MODULE)).append("\n"); // commands if (ms.numCommands() > 0) { sb.append(toTableRow("bin/", CODE)); ms.commands().entrySet().stream() .sorted(Map.Entry.comparingByKey()) .forEach(e -> sb.append(toTableRow(e.getKey(), e.getValue(), CODE))); sb.append(blankRow()); } // native libraries if (ms.numNativeLibraries() > 0) { sb.append(toTableRow("lib/", CODE)); ms.nativeLibs().entrySet().stream() .sorted(Map.Entry.comparingByKey()) .forEach(e -> sb.append(toTableRow(e.getKey(), e.getValue(), CODE))); sb.append(blankRow()); } // config files if (ms.numConfigs() > 0) { sb.append(toTableRow("conf/", CODE)); ms.configFiles().entrySet().stream() .sorted(Map.Entry.comparingByKey()) .forEach(e -> sb.append(toTableRow(e.getKey(), e.getValue(), CODE))); } // totals sb.append("
").append(""); return sb.toString(); } private String blankRow() { return toTableRow(" ", BR); } private String toTableRow(String col, Selector selector) { TableDataBuilder builder = new TableDataBuilder(); builder.colspan(selector, 2, col); return builder.build(); } private String toTableRow(String col1, long col2) { return toTableRow(col1, col2, BR); } private String toTableRow(String col1, long col2, Selector selector) { TableDataBuilder builder = new TableDataBuilder(); builder.data(selector, col1); builder.data(col2); return builder.build(); } private String toTableRow(String singular, String plural, int count, long bytes) { if (count == 0) { return ""; } TableDataBuilder builder = new TableDataBuilder(); if (count == 1) { builder.data(count + " " + singular); } else { builder.data(count + " " + plural); } builder.data(bytes); return builder.build(); } class TableDataBuilder { private final StringBuilder sb; TableDataBuilder() { this.sb = new StringBuilder(""); } TableDataBuilder data(String s) { data(BR, s); return this; } TableDataBuilder data(long num) { data(NUMBER, String.format("%,d", num)); return this; } TableDataBuilder colspan(Selector selector, int columns, String data) { sb.append(""); sb.append(""); sb.append(data).append(""); return this; } TableDataBuilder data(Selector selector, String data) { sb.append(""); sb.append(data).append(""); return this; } String build() { sb.append(""); return sb.toString(); } } } private static final String[] STYLES = new String[]{ "", "", }; } }