Skip to content

Commit

Permalink
Caching for CUPS status performance (#6)
Browse files Browse the repository at this point in the history
Caching for CUPS status performance

---------

Co-authored-by: Vzor- <Kyle@Berezin.com>
  • Loading branch information
tresf and Vzor- authored Oct 6, 2023
1 parent 59b3d85 commit d5524a7
Show file tree
Hide file tree
Showing 4 changed files with 249 additions and 5 deletions.
96 changes: 96 additions & 0 deletions src/qz/common/CachedObject.java
Original file line number Diff line number Diff line change
@@ -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 <T> The type of object to be cached.
*/
public class CachedObject<T> {
public static final long DEFAULT_LIFESPAN = 5000; // in milliseconds
T lastObject;
Supplier<T> 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<T> 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<T> 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<T> 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<Integer> cachedString = new CachedObject<>(testInt::incrementAndGet);
for(int i = 0; i < 100; i++) {
Thread.sleep(1500);
System.out.println(cachedString.get());
}
}
}
22 changes: 19 additions & 3 deletions src/qz/printer/PrintServiceMatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
Expand All @@ -45,14 +58,15 @@ public static NativePrinterMap getNativePrinterList() {
}

public static NativePrinter getDefaultPrinter() {
PrintService defaultService = PrintServiceLookup.lookupDefaultPrintService();
PrintService defaultService = lookupDefaultPrintService();

if(defaultService == null) {
return null;
}

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);
}

Expand Down Expand Up @@ -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"); }
Expand Down Expand Up @@ -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;

Expand Down
130 changes: 130 additions & 0 deletions src/qz/printer/info/CachedPrintService.java
Original file line number Diff line number Diff line change
@@ -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<PrintService> cachedDefault = new CachedObject<>(CachedPrintService::innerLookupDefaultPrintService, lifespan);
private static final CachedObject<PrintService[]> cachedPrintServices = new CachedObject<>(CachedPrintService::innerLookupPrintServices, lifespan);
private final CachedObject<String> cachedName;
private final CachedObject<PrintServiceAttributeSet> cachedAttributeSet;
private final HashMap<Class<?>, 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 extends PrintServiceAttribute> T getAttribute(Class<T> category) {
if (!cachedAttributes.containsKey(category)) {
Supplier<T> supplier = () -> innerPrintService.getAttribute(category);
CachedObject<T> 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<? extends Attribute> category) {
return innerPrintService.isAttributeCategorySupported(category);
}

@Override
public Object getDefaultAttributeValue(Class<? extends Attribute> category) {
return innerPrintService.getDefaultAttributeValue(category);
}

@Override
public Object getSupportedAttributeValues(Class<? extends Attribute> 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();
}
}
6 changes: 4 additions & 2 deletions src/qz/printer/info/NativePrinter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -69,6 +70,7 @@ public boolean equals(Object o) {
}

private final String printerId;

private boolean outdated;
private PrinterProperty<String> description;
private PrinterProperty<PrintService> printService;
Expand Down

0 comments on commit d5524a7

Please sign in to comment.