From 4ea4845bafc45f1a67262bf2a9a6f1a653d2f968 Mon Sep 17 00:00:00 2001 From: asmhatre Date: Tue, 4 Aug 2026 17:33:49 +0000 Subject: [PATCH] Part 6 extra: the decision table measured on real data --- .../G04ThreeApproachesMeasured.java | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 src/main/java/com/ankurm/jackson3/part6streaming/G04ThreeApproachesMeasured.java diff --git a/src/main/java/com/ankurm/jackson3/part6streaming/G04ThreeApproachesMeasured.java b/src/main/java/com/ankurm/jackson3/part6streaming/G04ThreeApproachesMeasured.java new file mode 100644 index 0000000..56792ad --- /dev/null +++ b/src/main/java/com/ankurm/jackson3/part6streaming/G04ThreeApproachesMeasured.java @@ -0,0 +1,101 @@ +package com.ankurm.jackson3.part6streaming; + +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.core.json.JsonFactory; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.json.JsonMapper; + +import java.io.File; +import java.nio.file.Files; +import java.util.List; + +/** + * BEYOND THE POST — the decision table, measured. + * + * The post ends with a table claiming data binding "loads full object", tree model + * "loads full tree", and streaming uses "constant" memory. This generates a real + * file and measures all three so the table has numbers behind it. + * + * These are indicative single-shot measurements on one JVM, not JMH benchmarks — + * the ordering is the point, not the absolute figures. + */ +public class G04ThreeApproachesMeasured { + + public record LogEntry(long id, String level, String message) { } + + private static final int ENTRIES = 200_000; + + public static void main(String[] args) throws Exception { + File file = generate(); + System.out.println("input file : " + (file.length() / 1024 / 1024) + " MB, " + + ENTRIES + " entries"); + System.out.println(); + + JsonMapper mapper = JsonMapper.builder().build(); + + measure("data binding (readValue)", () -> { + List all = mapper.readValue(file, new TypeReference>() { }); + return all.stream().filter(e -> e.level().equals("ERROR")).count(); + }); + + measure("tree model (readTree)", () -> { + JsonNode root = mapper.readTree(file); + long n = 0; + for (JsonNode node : root) if ("ERROR".equals(node.path("level").asString())) n++; + return n; + }); + + measure("streaming (JsonParser)", () -> { + long n = 0; + JsonFactory factory = new JsonFactory(); + try (JsonParser p = factory.createParser(tools.jackson.core.ObjectReadContext.empty(), file)) { + p.nextToken(); + while (p.nextToken() != JsonToken.END_ARRAY) { + String level = null; + while (p.nextToken() != JsonToken.END_OBJECT) { + String f = p.currentName(); + p.nextToken(); + if ("level".equals(f)) level = p.getString(); + } + if ("ERROR".equals(level)) n++; + } + } + return n; + }); + + file.delete(); + } + + private interface Counter { long count() throws Exception; } + + private static void measure(String label, Counter counter) throws Exception { + Runtime rt = Runtime.getRuntime(); + System.gc(); + Thread.sleep(120); + long heapBefore = rt.totalMemory() - rt.freeMemory(); + long start = System.nanoTime(); + long errors = counter.count(); + long ms = (System.nanoTime() - start) / 1_000_000; + long heapAfter = rt.totalMemory() - rt.freeMemory(); + System.out.printf("%-28s errors=%-6d %5d ms heap delta %5d MB%n", + label, errors, ms, (heapAfter - heapBefore) / 1024 / 1024); + } + + private static File generate() throws Exception { + File f = File.createTempFile("logs", ".json"); + f.deleteOnExit(); + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < ENTRIES; i++) { + if (i > 0) sb.append(','); + sb.append("{\"id\":").append(i) + .append(",\"level\":\"").append(i % 50 == 0 ? "ERROR" : "INFO") + .append("\",\"message\":\"event number ").append(i) + .append(" with some padding to make the payload realistic\"}"); + } + sb.append(']'); + Files.writeString(f.toPath(), sb); + return f; + } +}