Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Caching for CUPS status performance #6

Merged
merged 13 commits into from
Oct 6, 2023
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 @@ -30,15 +31,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 @@ -48,14 +61,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 @@ -84,6 +98,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 @@ -156,7 +172,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