Skip to content

Commit ee32f8e

Browse files
brunoborgesCopilot
andcommitted
refactor: split generateog.java into modules, replace Batik with Graphics2D
- Split monolithic generateog.java into 7 source files under og/ package: Palette, Layout, ContentLoader, SyntaxHighlighter, SvgRenderer, PngRenderer, FontManager (using JBang //SOURCES directive) - Replace Apache Batik SVG transcoder with direct Graphics2D PNG rendering (eliminates batik-transcoder + batik-codec dependencies) - Load fonts directly from .ttf files via FontManager.getFont() for correct rendering of Inter weights and Unicode symbols (✗/✓) - Reuse single BufferedImage across all cards (34 GCs → 1) - Add -XX:Tier4CompileThreshold=100 to all java -jar execution calls in deploy.yml and benchmark.yml (~7% speedup for short-lived CLI apps) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 7273f4a commit ee32f8e

File tree

10 files changed

+632
-383
lines changed

10 files changed

+632
-383
lines changed

.github/workflows/benchmark.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,14 +97,14 @@ jobs:
9797
# HTML generator
9898
PY_STEADY=$(avg_runs $STEADY_RUNS python3 html-generators/generate.py)
9999
JBANG_STEADY=$(avg_runs $STEADY_RUNS jbang html-generators/generate.java)
100-
JAR_STEADY=$(avg_runs $STEADY_RUNS java -jar "$JAR")
101-
AOT_STEADY=$(avg_runs $STEADY_RUNS java -XX:AOTCache="$AOT" -jar "$JAR")
100+
JAR_STEADY=$(avg_runs $STEADY_RUNS java -XX:Tier4CompileThreshold=100 -jar "$JAR")
101+
AOT_STEADY=$(avg_runs $STEADY_RUNS java -XX:Tier4CompileThreshold=100 -XX:AOTCache="$AOT" -jar "$JAR")
102102
103103
# OG generator
104104
OG_PY_STEADY=$(avg_runs $STEADY_RUNS python3 html-generators/generateog.py)
105105
OG_JBANG_STEADY=$(avg_runs $STEADY_RUNS jbang html-generators/generateog.java)
106-
OG_JAR_STEADY=$(avg_runs $STEADY_RUNS java -jar "$OG_JAR")
107-
OG_AOT_STEADY=$(avg_runs $STEADY_RUNS java -XX:AOTCache="$OG_AOT" -jar "$OG_JAR")
106+
OG_JAR_STEADY=$(avg_runs $STEADY_RUNS java -XX:Tier4CompileThreshold=100 -jar "$OG_JAR")
107+
OG_AOT_STEADY=$(avg_runs $STEADY_RUNS java -XX:Tier4CompileThreshold=100 -XX:AOTCache="$OG_AOT" -jar "$OG_JAR")
108108
109109
# Write to GitHub Actions Job Summary
110110
{
@@ -238,13 +238,13 @@ jobs:
238238
239239
# HTML generator
240240
PY_CI=$(measure python3 html-generators/generate.py)
241-
JAR_CI=$(measure java -jar html-generators/generate.jar)
242-
AOT_CI=$(measure java -XX:AOTCache=html-generators/generate.aot -jar html-generators/generate.jar)
241+
JAR_CI=$(measure java -XX:Tier4CompileThreshold=100 -jar html-generators/generate.jar)
242+
AOT_CI=$(measure java -XX:Tier4CompileThreshold=100 -XX:AOTCache=html-generators/generate.aot -jar html-generators/generate.jar)
243243
244244
# OG generator
245245
OG_PY_CI=$(measure python3 html-generators/generateog.py)
246-
OG_JAR_CI=$(measure java -jar html-generators/generateog.jar)
247-
OG_AOT_CI=$(measure java -XX:AOTCache=html-generators/generateog.aot -jar html-generators/generateog.jar)
246+
OG_JAR_CI=$(measure java -XX:Tier4CompileThreshold=100 -jar html-generators/generateog.jar)
247+
OG_AOT_CI=$(measure java -XX:Tier4CompileThreshold=100 -XX:AOTCache=html-generators/generateog.aot -jar html-generators/generateog.jar)
248248
249249
{
250250
echo "## CI Cold Start — \`$os_name\`"

.github/workflows/deploy.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ jobs:
8484

8585
- name: Generate HTML with cached JAR + AOT
8686
if: steps.changes.outputs.generate == 'true' && steps.cache-restore.outputs.cache-hit == 'true'
87-
run: java -XX:AOTCache=html-generators/generate.aot -jar html-generators/generate.jar
87+
run: java -XX:Tier4CompileThreshold=100 -XX:AOTCache=html-generators/generate.aot -jar html-generators/generate.jar
8888

8989
- name: Setup JBang (cache miss)
9090
if: steps.changes.outputs.generate == 'true' && steps.cache-restore.outputs.cache-hit != 'true'
@@ -107,7 +107,7 @@ jobs:
107107

108108
- name: Generate OG cards with cached JAR + AOT
109109
if: steps.changes.outputs.og == 'true' && steps.cache-restore-og.outputs.cache-hit == 'true'
110-
run: java -XX:AOTCache=html-generators/generateog.aot -jar html-generators/generateog.jar
110+
run: java -XX:Tier4CompileThreshold=100 -XX:AOTCache=html-generators/generateog.aot -jar html-generators/generateog.jar
111111

112112
- name: Generate OG cards with JBang (cache miss)
113113
if: steps.changes.outputs.og == 'true' && steps.cache-restore-og.outputs.cache-hit != 'true'

html-generators/generateog.java

Lines changed: 16 additions & 373 deletions
Large diffs are not rendered by default.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package og;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
6+
7+
import java.io.IOException;
8+
import java.io.UncheckedIOException;
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
import java.util.ArrayList;
12+
import java.util.LinkedHashMap;
13+
import java.util.Map;
14+
import java.util.SequencedMap;
15+
16+
/** Loads content JSON/YAML files and category properties. */
17+
public final class ContentLoader {
18+
19+
static final String CONTENT_DIR = "content";
20+
static final String CATEGORIES_FILE = "html-generators/categories.properties";
21+
22+
static final ObjectMapper JSON_MAPPER = new ObjectMapper();
23+
static final ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory());
24+
static final Map<String, ObjectMapper> MAPPERS = Map.of(
25+
"json", JSON_MAPPER, "yaml", YAML_MAPPER, "yml", YAML_MAPPER
26+
);
27+
28+
public static final SequencedMap<String, String> CATEGORY_DISPLAY = loadProperties(CATEGORIES_FILE);
29+
30+
public record Snippet(JsonNode node) {
31+
public String get(String f) { return node.get(f).asText(); }
32+
public String slug() { return get("slug"); }
33+
public String category() { return get("category"); }
34+
public String title() { return get("title"); }
35+
public String jdkVersion() { return get("jdkVersion"); }
36+
public String oldCode() { return get("oldCode"); }
37+
public String modernCode() { return get("modernCode"); }
38+
public String oldApproach() { return get("oldApproach"); }
39+
public String modernApproach() { return get("modernApproach"); }
40+
public String oldLabel() { return get("oldLabel"); }
41+
public String modernLabel() { return get("modernLabel"); }
42+
public String key() { return category() + "/" + slug(); }
43+
public String catDisplay() { return CATEGORY_DISPLAY.get(category()); }
44+
}
45+
46+
public static SequencedMap<String, Snippet> loadAllSnippets() throws IOException {
47+
var snippets = new LinkedHashMap<String, Snippet>();
48+
for (var cat : CATEGORY_DISPLAY.sequencedKeySet()) {
49+
var catDir = Path.of(CONTENT_DIR, cat);
50+
if (!Files.isDirectory(catDir)) continue;
51+
var sorted = new ArrayList<Path>();
52+
for (var ext : MAPPERS.keySet()) {
53+
try (var stream = Files.newDirectoryStream(catDir, "*." + ext)) {
54+
stream.forEach(sorted::add);
55+
}
56+
}
57+
sorted.sort(Path::compareTo);
58+
for (var path : sorted) {
59+
var ext = path.getFileName().toString();
60+
ext = ext.substring(ext.lastIndexOf('.') + 1);
61+
var snippet = new Snippet(MAPPERS.get(ext).readTree(Files.readString(path)));
62+
snippets.put(snippet.key(), snippet);
63+
}
64+
}
65+
return snippets;
66+
}
67+
68+
public static SequencedMap<String, String> loadProperties(String file) {
69+
try {
70+
var map = new LinkedHashMap<String, String>();
71+
for (var line : Files.readAllLines(Path.of(file))) {
72+
line = line.strip();
73+
if (line.isEmpty() || line.startsWith("#")) continue;
74+
var idx = line.indexOf('=');
75+
if (idx > 0) map.put(line.substring(0, idx).strip(), line.substring(idx + 1).strip());
76+
}
77+
return map;
78+
} catch (IOException e) { throw new UncheckedIOException(e); }
79+
}
80+
81+
public static String xmlEscape(String s) {
82+
return s == null ? ""
83+
: s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
84+
.replace("\"", "&quot;").replace("'", "&apos;");
85+
}
86+
87+
private ContentLoader() {}
88+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package og;
2+
3+
import java.awt.Font;
4+
import java.awt.FontFormatException;
5+
import java.awt.GraphicsEnvironment;
6+
import java.io.IOException;
7+
import java.net.URI;
8+
import java.nio.file.Files;
9+
import java.nio.file.Path;
10+
import java.util.Map;
11+
12+
/** Downloads and registers Inter + JetBrains Mono fonts. */
13+
public final class FontManager {
14+
15+
static final Path FONT_CACHE = Path.of(
16+
System.getProperty("user.home"), ".cache", "javaevolved-fonts");
17+
18+
static final Map<String, String> FONT_URLS = Map.of(
19+
"Inter-Regular.ttf",
20+
"https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfMZg.ttf",
21+
"Inter-Medium.ttf",
22+
"https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuI6fMZg.ttf",
23+
"Inter-SemiBold.ttf",
24+
"https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuGKYMZg.ttf",
25+
"Inter-Bold.ttf",
26+
"https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYMZg.ttf",
27+
"JetBrainsMono-Regular.ttf",
28+
"https://fonts.gstatic.com/s/jetbrainsmono/v24/tDbY2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKxjPQ.ttf",
29+
"JetBrainsMono-Medium.ttf",
30+
"https://fonts.gstatic.com/s/jetbrainsmono/v24/tDbY2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8-qxjPQ.ttf"
31+
);
32+
33+
/** Download fonts to cache and register with Java's graphics environment. */
34+
public static void ensureFonts() throws IOException {
35+
Files.createDirectories(FONT_CACHE);
36+
var ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
37+
for (var entry : FONT_URLS.entrySet()) {
38+
var file = FONT_CACHE.resolve(entry.getKey());
39+
if (!Files.exists(file)) {
40+
System.out.println("Downloading %s...".formatted(entry.getKey()));
41+
try (var in = URI.create(entry.getValue()).toURL().openStream()) {
42+
Files.copy(in, file);
43+
}
44+
}
45+
try {
46+
var font = Font.createFont(Font.TRUETYPE_FONT, file.toFile());
47+
ge.registerFont(font);
48+
} catch (FontFormatException e) {
49+
System.out.println("[WARN] Could not register font %s: %s"
50+
.formatted(entry.getKey(), e.getMessage()));
51+
}
52+
}
53+
}
54+
55+
/** Load a specific font file at the given size. */
56+
public static Font getFont(String filename, float size) {
57+
try {
58+
var path = FONT_CACHE.resolve(filename);
59+
return Font.createFont(Font.TRUETYPE_FONT, path.toFile()).deriveFont(size);
60+
} catch (FontFormatException | IOException e) {
61+
throw new RuntimeException("Cannot load font: " + filename, e);
62+
}
63+
}
64+
65+
private FontManager() {}
66+
}

html-generators/og/Layout.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package og;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
6+
/** Card dimensions and code-fitting helpers. */
7+
public final class Layout {
8+
9+
public static final int W = 1200, H = 630;
10+
public static final int PAD = 40;
11+
public static final int HEADER_H = 100;
12+
public static final int FOOTER_H = 56;
13+
public static final int CODE_TOP = HEADER_H;
14+
public static final int CODE_H = H - HEADER_H - FOOTER_H;
15+
public static final int COL_W = (W - PAD * 2 - 20) / 2; // 20px gap between panels
16+
public static final int CODE_PAD = 14;
17+
public static final int LABEL_H = 32;
18+
public static final int USABLE_W = COL_W - CODE_PAD * 2;
19+
public static final int USABLE_H = CODE_H - LABEL_H - CODE_PAD;
20+
public static final double CHAR_WIDTH_RATIO = 0.6;
21+
public static final double LINE_HEIGHT_RATIO = 1.55;
22+
public static final int MIN_CODE_FONT = 9;
23+
public static final int MAX_CODE_FONT = 16;
24+
25+
/** Compute the best font size (MIN–MAX) that fits both code blocks. */
26+
public static int bestFontSize(List<String> oldLines, List<String> modernLines) {
27+
int maxChars = Math.max(
28+
oldLines.stream().mapToInt(String::length).max().orElse(1),
29+
modernLines.stream().mapToInt(String::length).max().orElse(1)
30+
);
31+
int maxLines = Math.max(oldLines.size(), modernLines.size());
32+
int byWidth = (int) (USABLE_W / (maxChars * CHAR_WIDTH_RATIO));
33+
int byHeight = (int) (USABLE_H / (maxLines * LINE_HEIGHT_RATIO));
34+
return Math.max(MIN_CODE_FONT, Math.min(MAX_CODE_FONT, Math.min(byWidth, byHeight)));
35+
}
36+
37+
/** Truncate lines to fit the panel height at the given font size. */
38+
public static List<String> fitLines(List<String> lines, int fontSize) {
39+
int lineH = (int) (fontSize * LINE_HEIGHT_RATIO);
40+
int maxLines = USABLE_H / lineH;
41+
if (lines.size() <= maxLines) return lines;
42+
var truncated = new ArrayList<>(lines.subList(0, maxLines - 1));
43+
truncated.add("...");
44+
return truncated;
45+
}
46+
47+
private Layout() {}
48+
}

html-generators/og/Palette.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package og;
2+
3+
import java.awt.Color;
4+
5+
/** Light-theme color palette for OG cards. */
6+
public final class Palette {
7+
8+
// Background & chrome
9+
public static final String BG = "#ffffff";
10+
public static final String BORDER = "#d8d8e0";
11+
public static final String TEXT = "#1a1a2e";
12+
public static final String TEXT_MUTED = "#6b7280";
13+
public static final String OLD_BG = "#fef2f2";
14+
public static final String MODERN_BG = "#eff6ff";
15+
public static final String OLD_ACCENT = "#dc2626";
16+
public static final String GREEN = "#059669";
17+
public static final String ACCENT = "#6366f1";
18+
public static final String BADGE_BG = "#f3f4f6";
19+
20+
// Syntax highlighting (VS Code light-inspired)
21+
public static final String SYN_KEYWORD = "#7c3aed"; // purple
22+
public static final String SYN_TYPE = "#0e7490"; // teal
23+
public static final String SYN_STRING = "#059669"; // green
24+
public static final String SYN_COMMENT = "#6b7280"; // gray
25+
public static final String SYN_ANNOTATION = "#b45309"; // amber
26+
public static final String SYN_NUMBER = "#c2410c"; // orange
27+
public static final String SYN_DEFAULT = "#1a1a2e"; // dark
28+
29+
public static Color color(String hex) {
30+
return Color.decode(hex);
31+
}
32+
33+
private Palette() {}
34+
}

0 commit comments

Comments
 (0)