From d5524a77c73961c9d1c99840bdb7ff3b017a4d85 Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Fri, 6 Oct 2023 14:06:45 -0400 Subject: [PATCH] Caching for CUPS status performance (#6) Caching for CUPS status performance --------- Co-authored-by: Vzor- --- src/qz/common/CachedObject.java | 96 +++++++++++++++ src/qz/printer/PrintServiceMatcher.java | 22 +++- src/qz/printer/info/CachedPrintService.java | 130 ++++++++++++++++++++ src/qz/printer/info/NativePrinter.java | 6 +- 4 files changed, 249 insertions(+), 5 deletions(-) create mode 100644 src/qz/common/CachedObject.java create mode 100644 src/qz/printer/info/CachedPrintService.java diff --git a/src/qz/common/CachedObject.java b/src/qz/common/CachedObject.java new file mode 100644 index 000000000..f9a5b3804 --- /dev/null +++ b/src/qz/common/CachedObject.java @@ -0,0 +1,96 @@ +package qz.common; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +/** + * A generic class that encapsulates an object for caching. The cached object + * will be refreshed automatically when accessed after its lifespan has expired. + * + * @param The type of object to be cached. + */ +public class CachedObject { + public static final long DEFAULT_LIFESPAN = 5000; // in milliseconds + T lastObject; + Supplier supplier; + private long timestamp; + private long lifespan; + + /** + * Creates a new CachedObject with a default lifespan of 5000 milliseconds + * + * @param supplier The function to pull new values from + */ + public CachedObject(Supplier supplier) { + this(supplier, DEFAULT_LIFESPAN); + } + + /** + * Creates a new CachedObject + * + * @param supplier The function to pull new values from + * @param lifespan The lifespan of the cached object in milliseconds + */ + public CachedObject(Supplier supplier, long lifespan) { + this.supplier = supplier; + setLifespan(lifespan); + timestamp = Long.MIN_VALUE; // System.nanoTime() can be negative, MIN_VALUE guarantees a first-run. + } + + /** + * Registers a new supplier for the CachedObject + * + * @param supplier The function to pull new values from + */ + public void registerSupplier(Supplier supplier) { + this.supplier = supplier; + } + + /** + * Sets the lifespan of the cached object + * + * @param milliseconds The lifespan of the cached object in milliseconds + */ + public void setLifespan(long milliseconds) { + lifespan = Math.max(0, milliseconds); // prevent overflow + } + + /** + * Retrieves the cached object. + * If the cached object's lifespan has expired, it gets refreshed before being returned. + * + * @return The cached object + */ + public T get() { + return get(false); + } + + /** + * Retrieves the cached object. + * If the cached object's lifespan is expired or forceRefresh is true, it gets refreshed before being returned. + * + * @param forceRefresh If true, the cached object will be refreshed before being returned regardless of its lifespan + * @return The cached object + */ + public T get(boolean forceRefresh) { + long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()); + // check lifespan + if (forceRefresh || (timestamp + lifespan <= now)) { + timestamp = now; + lastObject = supplier.get(); + } + return lastObject; + } + + // Test + public static void main(String ... args) throws InterruptedException { + final AtomicInteger testInt = new AtomicInteger(0); + + CachedObject cachedString = new CachedObject<>(testInt::incrementAndGet); + for(int i = 0; i < 100; i++) { + Thread.sleep(1500); + System.out.println(cachedString.get()); + } + } +} diff --git a/src/qz/printer/PrintServiceMatcher.java b/src/qz/printer/PrintServiceMatcher.java index e77a29d42..5952547a0 100644 --- a/src/qz/printer/PrintServiceMatcher.java +++ b/src/qz/printer/PrintServiceMatcher.java @@ -15,6 +15,7 @@ import org.codehaus.jettison.json.JSONArray; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; +import qz.printer.info.CachedPrintService; import qz.printer.info.NativePrinter; import qz.printer.info.NativePrinterMap; import qz.utils.SystemUtilities; @@ -27,15 +28,27 @@ public class PrintServiceMatcher { private static final Logger log = LogManager.getLogger(PrintServiceMatcher.class); + //todo: include jdk version test for caching when JDK-7001133 is resolved + private static final boolean useCache = SystemUtilities.isMac(); // PrintService is slow, use a cache instead per JDK-7001133 public static NativePrinterMap getNativePrinterList(boolean silent, boolean withAttributes) { NativePrinterMap printers = NativePrinterMap.getInstance(); - printers.putAll(PrintServiceLookup.lookupPrintServices(null, null)); + printers.putAll(lookupPrintServices()); if (withAttributes) { printers.values().forEach(NativePrinter::getDriverAttributes); } if (!silent) { log.debug("Found {} printers", printers.size()); } return printers; } + private static PrintService[] lookupPrintServices() { + if (useCache) return CachedPrintService.lookupPrintServices(); + return PrintServiceLookup.lookupPrintServices(null, null); + } + + private static PrintService lookupDefaultPrintService() { + if (useCache) return CachedPrintService.lookupDefaultPrintService(); + return PrintServiceLookup.lookupDefaultPrintService(); + } + public static NativePrinterMap getNativePrinterList(boolean silent) { return getNativePrinterList(silent, false); } @@ -45,7 +58,7 @@ public static NativePrinterMap getNativePrinterList() { } public static NativePrinter getDefaultPrinter() { - PrintService defaultService = PrintServiceLookup.lookupDefaultPrintService(); + PrintService defaultService = lookupDefaultPrintService(); if(defaultService == null) { return null; @@ -53,6 +66,7 @@ public static NativePrinter getDefaultPrinter() { NativePrinterMap printers = NativePrinterMap.getInstance(); if (!printers.contains(defaultService)) { + //todo: is this working correctly? it seems to set the printers list = to [1] {defaultPrinter} printers.putAll(defaultService); } @@ -81,6 +95,8 @@ public static NativePrinter matchPrinter(String printerSearch, boolean silent) { if (!silent) { log.debug("Searching for PrintService matching {}", printerSearch); } + // Fix for https://github.com/qzind/tray/issues/931 + // This is more than an optimization, removal will lead to a regression NativePrinter defaultPrinter = getDefaultPrinter(); if (defaultPrinter != null && printerSearch.equals(defaultPrinter.getName())) { if (!silent) { log.debug("Matched default printer, skipping further search"); } @@ -153,7 +169,7 @@ public static NativePrinter matchPrinter(String printerSearch) { public static JSONArray getPrintersJSON(boolean includeDetails) throws JSONException { JSONArray list = new JSONArray(); - PrintService defaultService = PrintServiceLookup.lookupDefaultPrintService(); + PrintService defaultService = lookupDefaultPrintService(); boolean mediaTrayCrawled = false; diff --git a/src/qz/printer/info/CachedPrintService.java b/src/qz/printer/info/CachedPrintService.java new file mode 100644 index 000000000..445435710 --- /dev/null +++ b/src/qz/printer/info/CachedPrintService.java @@ -0,0 +1,130 @@ +package qz.printer.info; + +import qz.common.CachedObject; + +import javax.print.*; +import javax.print.attribute.Attribute; +import javax.print.attribute.AttributeSet; +import javax.print.attribute.PrintServiceAttribute; +import javax.print.attribute.PrintServiceAttributeSet; +import javax.print.event.PrintServiceAttributeListener; +import java.util.HashMap; +import java.util.function.Supplier; + +public class CachedPrintService implements PrintService { + public PrintService innerPrintService; + // PrintService.getName() is slow, use a cache instead per JDK-7001133 + // TODO: Remove this comment when upstream bug report is filed + private static final long lifespan = CachedObject.DEFAULT_LIFESPAN; + private static final CachedObject cachedDefault = new CachedObject<>(CachedPrintService::innerLookupDefaultPrintService, lifespan); + private static final CachedObject cachedPrintServices = new CachedObject<>(CachedPrintService::innerLookupPrintServices, lifespan); + private final CachedObject cachedName; + private final CachedObject cachedAttributeSet; + private final HashMap, CachedObject> cachedAttributes = new HashMap<>(); + + public static PrintService lookupDefaultPrintService() { + return cachedDefault.get(); + } + + public static PrintService[] lookupPrintServices() { + return cachedPrintServices.get(); + } + + private static PrintService innerLookupDefaultPrintService() { + return new CachedPrintService(PrintServiceLookup.lookupDefaultPrintService()); + } + + private static PrintService[] innerLookupPrintServices() { + PrintService[] printServices = PrintServiceLookup.lookupPrintServices(null, null); + for (int i = 0; i < printServices.length; i++) { + printServices[i] = new CachedPrintService(printServices[i]); + } + return printServices; + } + + public CachedPrintService(PrintService printService) { + innerPrintService = printService; + cachedName = new CachedObject<>(innerPrintService::getName, lifespan); + cachedAttributeSet = new CachedObject<>(innerPrintService::getAttributes, lifespan); + } + + @Override + public String getName() { + return cachedName.get(); + } + + @Override + public DocPrintJob createPrintJob() { + return innerPrintService.createPrintJob(); + } + + @Override + public void addPrintServiceAttributeListener(PrintServiceAttributeListener listener) { + innerPrintService.addPrintServiceAttributeListener(listener); + } + + @Override + public void removePrintServiceAttributeListener(PrintServiceAttributeListener listener) { + innerPrintService.removePrintServiceAttributeListener(listener); + } + + @Override + public PrintServiceAttributeSet getAttributes() { + return cachedAttributeSet.get(); + } + + @Override + public T getAttribute(Class category) { + if (!cachedAttributes.containsKey(category)) { + Supplier supplier = () -> innerPrintService.getAttribute(category); + CachedObject cachedObject = new CachedObject<>(supplier, lifespan); + cachedAttributes.put(category, cachedObject); + } + return category.cast(cachedAttributes.get(category).get()); + } + + @Override + public DocFlavor[] getSupportedDocFlavors() { + return innerPrintService.getSupportedDocFlavors(); + } + + @Override + public boolean isDocFlavorSupported(DocFlavor flavor) { + return innerPrintService.isDocFlavorSupported(flavor); + } + + @Override + public Class[] getSupportedAttributeCategories() { + return innerPrintService.getSupportedAttributeCategories(); + } + + @Override + public boolean isAttributeCategorySupported(Class category) { + return innerPrintService.isAttributeCategorySupported(category); + } + + @Override + public Object getDefaultAttributeValue(Class category) { + return innerPrintService.getDefaultAttributeValue(category); + } + + @Override + public Object getSupportedAttributeValues(Class category, DocFlavor flavor, AttributeSet attributes) { + return innerPrintService.getSupportedAttributeValues(category, flavor, attributes); + } + + @Override + public boolean isAttributeValueSupported(Attribute attrval, DocFlavor flavor, AttributeSet attributes) { + return innerPrintService.isAttributeValueSupported(attrval, flavor, attributes); + } + + @Override + public AttributeSet getUnsupportedAttributes(DocFlavor flavor, AttributeSet attributes) { + return innerPrintService.getUnsupportedAttributes(flavor, attributes); + } + + @Override + public ServiceUIFactory getServiceUIFactory() { + return innerPrintService.getServiceUIFactory(); + } +} diff --git a/src/qz/printer/info/NativePrinter.java b/src/qz/printer/info/NativePrinter.java index 85b64f832..e1d03f356 100644 --- a/src/qz/printer/info/NativePrinter.java +++ b/src/qz/printer/info/NativePrinter.java @@ -57,9 +57,10 @@ public boolean isNull() { @Override public boolean equals(Object o) { - // PrintService.equals(...) is very slow in CUPS; use the pointer + // PrintService.equals(...) is very slow in CUPS; use the pointer instead per JDK-7001133 if (SystemUtilities.isUnix() && value instanceof PrintService) { - return o == value; + //todo this needs to be more than a name check. maybe use attribute set + return ((PrintService)value).getName().equals(getName()); } if (value != null) { return value.equals(o); @@ -69,6 +70,7 @@ public boolean equals(Object o) { } private final String printerId; + private boolean outdated; private PrinterProperty description; private PrinterProperty printService;