Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.demcha.compose;

import com.demcha.compose.document.api.DocumentPageSize;
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.backend.fixed.pdf.PdfMeasurementResources;
import com.demcha.compose.document.layout.DocumentGraph;
import com.demcha.compose.document.layout.DocumentLayoutPassContext;
import com.demcha.compose.document.layout.LayoutCanvas;
import com.demcha.compose.document.layout.LayoutCompiler;
import com.demcha.compose.document.layout.LayoutGraph;
import com.demcha.compose.document.layout.NodeRegistry;
import com.demcha.compose.document.node.DocumentNode;

import java.util.List;

/**
* Finding 12 probe — counts the text-width measurements the layout makes to
* resolve auto-size paragraphs.
*
* <p>Auto-size shrinks the font over a size grid until the line fits. This probe
* compiles a page of auto-size headings through a {@link CountingTextMeasurementSystem}
* and reports the {@code textWidth} request count, so a develop (linear scan) vs
* branch (binary search) A/B shows the measurement reduction directly. No
* {@code src/main} changes.</p>
*/
public final class AutoSizeMeasureProbe {

public static void main(String[] args) throws Exception {
BenchmarkSupport.configureQuietLogging();

int autoSizeParagraphs = 8;
try (DocumentSession session = GraphCompose.document()
.pageSize(DocumentPageSize.of(220, 1200))
.margin(10, 10, 10, 10)
.create()) {
session.pageFlow(flow -> {
for (int i = 0; i < autoSizeParagraphs; i++) {
// 36pt heading that must shrink to ~16pt to fit the ~200pt inner
// width: the linear scan steps 36 -> 16 by 0.5 (~40 measures),
// the binary search resolves the same size in ~log2(n).
flow.addParagraph(p -> p
.text("Auto-size headline that should fit on a single line")
.autoSize(36, 6));
}
});

List<DocumentNode> roots = session.roots();
LayoutCanvas canvas = session.canvas();
NodeRegistry registry = session.registry();

try (PdfMeasurementResources resources = PdfMeasurementResources.open(List.of())) {
CountingTextMeasurementSystem counter =
new CountingTextMeasurementSystem(resources.textMeasurementSystem());
DocumentLayoutPassContext context = new DocumentLayoutPassContext(
registry, canvas, resources.fontLibrary(), counter, false);
LayoutCompiler compiler = new LayoutCompiler(registry);
LayoutGraph layout = compiler.compile(new DocumentGraph(roots), context, context);

CountingTextMeasurementSystem.Counts c = counter.snapshot();
System.out.println("GraphCompose Finding-12 Auto-Size Measurement Probe");
System.out.printf("auto-size paragraphs: %d, pages: %d%n",
autoSizeParagraphs, layout.totalPages());
System.out.printf("textWidth requests during compile (auto-size grid search + wrap): %d%n",
c.widthRequests());
System.out.printf("per auto-size paragraph: %.1f%n",
c.widthRequests() / (double) autoSizeParagraphs);
}
}
}
}
102 changes: 102 additions & 0 deletions benchmarks/src/main/java/com/demcha/compose/RenderOperatorProbe.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.demcha.compose;

import com.demcha.compose.document.api.DocumentPageSize;
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.style.DocumentColor;
import com.demcha.compose.document.style.DocumentTextDecoration;
import com.demcha.compose.document.style.DocumentTextStyle;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.contentstream.operator.Operator;
import org.apache.pdfbox.pdfparser.PDFStreamParser;
import org.apache.pdfbox.pdmodel.PDDocument;

import java.awt.Color;
import java.io.IOException;
import java.util.List;
import java.util.function.Consumer;

/**
* Finding 5 probe — counts content-stream operators in rendered documents.
*
* <p>Before F5 the paragraph render handler wrote one {@code setFont} (Tf) and one
* {@code setNonStrokingColor} (rg/g) per text span, i.e. exactly one of each per
* text-show ({@code Tj}/{@code TJ}). So the pre-F5 Tf and colour counts equal the
* text-draw count — that is the deterministic baseline this probe prints alongside
* the post-F5 counts; no A/B build needed. Tables render through a different
* handler that F5 does not touch, included to show the honest boundary.</p>
*/
public final class RenderOperatorProbe {

private static final DocumentTextStyle BODY = DocumentTextStyle.builder()
.size(10).decoration(DocumentTextDecoration.DEFAULT)
.color(DocumentColor.of(new Color(40, 50, 60))).build();
private static final DocumentTextStyle HEAD = DocumentTextStyle.builder()
.size(16).decoration(DocumentTextDecoration.BOLD)
.color(DocumentColor.of(new Color(18, 40, 74))).build();

private static final String SENTENCE =
"GraphCompose lays out structured business documents across many pages "
+ "while keeping header and footer placement stable. ";

public static void main(String[] args) throws Exception {
BenchmarkSupport.configureQuietLogging();

System.out.println("GraphCompose Finding-5 Render-Operator Probe");
System.out.printf("%-22s | %8s | %8s | %8s | %12s | %9s%n",
"Scenario", "Draws", "Tf(now)", "rg(now)", "Tf+rg saved", "Reduction");
System.out.println("-".repeat(80));

report("long-paragraph", flow -> flow.addParagraph(p -> p.text(SENTENCE.repeat(40)).textStyle(BODY)));
report("20-paragraph body", flow -> {
for (int i = 0; i < 20; i++) {
flow.addParagraph(p -> p.text(SENTENCE.repeat(3)).textStyle(BODY));
}
});
report("headed sections", flow -> {
for (int i = 0; i < 8; i++) {
flow.addParagraph(p -> p.text("Section heading").textStyle(HEAD));
flow.addParagraph(p -> p.text(SENTENCE.repeat(4)).textStyle(BODY));
}
});
report("50-row table (F5 N/A)", flow -> flow.addTable(t -> {
t.autoColumns(4).header("Item", "Qty", "Unit", "Total");
for (int r = 1; r <= 50; r++) {
t.row("Line item " + r, "3", "ea", "38.75");
}
}));

System.out.println();
System.out.println("Pre-F5 Tf == rg == Draws (one font + one colour op per span). 'saved' = (Draws-Tf)+(Draws-rg).");
}

private static void report(String scenario, Consumer<com.demcha.compose.document.dsl.PageFlowBuilder> author) throws Exception {
byte[] pdf;
try (DocumentSession session = GraphCompose.document()
.pageSize(DocumentPageSize.A4).margin(28, 28, 28, 28).create()) {
session.pageFlow(author);
pdf = session.toPdfBytes();
}
try (PDDocument document = Loader.loadPDF(pdf)) {
int draws = count(document, "Tj") + count(document, "TJ");
int tf = count(document, "Tf");
int rg = count(document, "rg") + count(document, "g") + count(document, "sc") + count(document, "scn");
int saved = Math.max(0, draws - tf) + Math.max(0, draws - rg);
double reduction = draws == 0 ? 0 : 100.0 * (2.0 * draws - tf - rg) / (2.0 * draws);
System.out.printf("%-22s | %8d | %8d | %8d | %12d | %8.1f%%%n",
scenario, draws, tf, rg, saved, reduction);
}
}

private static int count(PDDocument document, String op) throws IOException {
int n = 0;
for (var page : document.getPages()) {
List<Object> tokens = new PDFStreamParser(page).parse();
for (Object token : tokens) {
if (token instanceof Operator operator && op.equals(operator.getName())) {
n++;
}
}
}
return n;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package com.demcha.compose;

import com.demcha.compose.document.api.DocumentPageSize;
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.backend.fixed.pdf.PdfMeasurementResources;
import com.demcha.compose.document.layout.DocumentGraph;
import com.demcha.compose.document.layout.DocumentLayoutPassContext;
import com.demcha.compose.document.layout.LayoutCanvas;
import com.demcha.compose.document.layout.LayoutCompiler;
import com.demcha.compose.document.layout.LayoutGraph;
import com.demcha.compose.document.layout.NodeRegistry;
import com.demcha.compose.document.node.DocumentNode;

import java.lang.management.ManagementFactory;
import java.util.Arrays;
import java.util.List;

/**
* Finding 10 probe — warm per-compile allocation of paginating one large table
* across many pages.
*
* <p>A table that spans N pages is split page-by-page: each split re-slices the
* shrinking tail via {@code TableLayoutSupport.sliceTablePreparedNode}. On the
* body-only path that slice currently re-copies the already-immutable row /
* row-height sub-lists ({@code List.copyOf} of a {@code subList}), so the
* cumulative copy work is O(rows x pages). This probe compiles a big multi-page
* table through the canonical {@link LayoutCompiler} and reports the warm
* (JIT-steady) bytes allocated by the compile pass, so a develop (copy) vs
* branch (sub-list view) A/B shows the slice-allocation reduction directly. No
* {@code src/main} changes.</p>
*/
public final class TablePaginationAllocProbe {

private static final com.sun.management.ThreadMXBean THREAD_MX =
(com.sun.management.ThreadMXBean) ManagementFactory.getThreadMXBean();

private static final int ROWS = 2500;
private static final int COLUMNS = 4;
private static final int WARMUP = 60;
private static final int MEASURE = 11;

public static void main(String[] args) throws Exception {
BenchmarkSupport.configureQuietLogging();
enableAllocationMeasurement();

try (DocumentSession session = GraphCompose.document()
.pageSize(DocumentPageSize.A4)
.margin(24, 24, 24, 24)
.create()) {
session.pageFlow(flow -> flow.addTable(table -> {
table.autoColumns(COLUMNS).header("Item", "Qty", "Price", "Total");
for (int row = 1; row <= ROWS; row++) {
table.row("Line item " + row, "3", "12.50", "38.75");
}
}));

List<DocumentNode> roots = session.roots();
LayoutCanvas canvas = session.canvas();
NodeRegistry registry = session.registry();

try (PdfMeasurementResources resources = PdfMeasurementResources.open(List.of())) {
LayoutCompiler compiler = new LayoutCompiler(registry);
DocumentGraph graph = new DocumentGraph(roots);

int pages = 0;
// Warm up the compile path so the measured allocation reflects
// JIT steady state, not class-load / first-call cold start.
for (int i = 0; i < WARMUP; i++) {
pages = compile(compiler, graph, registry, canvas, resources).totalPages();
}

long[] alloc = new long[MEASURE];
for (int m = 0; m < MEASURE; m++) {
long before = currentThreadAllocatedBytes();
LayoutGraph layout = compile(compiler, graph, registry, canvas, resources);
alloc[m] = before < 0 ? -1 : currentThreadAllocatedBytes() - before;
pages = layout.totalPages();
}
Arrays.sort(alloc);

System.out.println("GraphCompose Finding-10 Table-Pagination Allocation Probe");
System.out.printf("table: %d rows x %d cols, pages: %d (≈%d page splits)%n",
ROWS, COLUMNS, pages, Math.max(0, pages - 1));
System.out.printf("warm compile allocation (median of %d): %.1f KB%n",
MEASURE, alloc[MEASURE / 2] / 1024.0);
System.out.printf(" min %.1f KB / max %.1f KB%n",
alloc[0] / 1024.0, alloc[MEASURE - 1] / 1024.0);
}
}
}

private static LayoutGraph compile(LayoutCompiler compiler, DocumentGraph graph,
NodeRegistry registry, LayoutCanvas canvas,
PdfMeasurementResources resources) {
DocumentLayoutPassContext context = new DocumentLayoutPassContext(
registry, canvas, resources.fontLibrary(), resources.textMeasurementSystem(), false);
return compiler.compile(graph, context, context);
}

private static void enableAllocationMeasurement() {
try {
if (THREAD_MX.isThreadAllocatedMemorySupported() && !THREAD_MX.isThreadAllocatedMemoryEnabled()) {
THREAD_MX.setThreadAllocatedMemoryEnabled(true);
}
} catch (UnsupportedOperationException ignored) {
// Allocation measurement unsupported on this JVM; the probe reports n/a.
}
}

private static long currentThreadAllocatedBytes() {
try {
if (!THREAD_MX.isThreadAllocatedMemorySupported() || !THREAD_MX.isThreadAllocatedMemoryEnabled()) {
return -1;
}
} catch (UnsupportedOperationException ex) {
return -1;
}
return THREAD_MX.getCurrentThreadAllocatedBytes();
}
}