Skip to content

Commit

Permalink
Printer Status: Performance improvements (qzind#1124)
Browse files Browse the repository at this point in the history
Caching for CUPS status performance (#6)

---------
Co-authored-by: Vzor- <[email protected]>
  • Loading branch information
tresf authored Oct 12, 2023
1 parent 402f924 commit 186ac38
Show file tree
Hide file tree
Showing 8 changed files with 348 additions and 15 deletions.
97 changes: 97 additions & 0 deletions src/qz/common/CachedObject.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
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
*/
@SuppressWarnings("unused")
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());
}
}
}
25 changes: 21 additions & 4 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.CachedPrintServiceLookup;
import qz.printer.info.NativePrinter;
import qz.printer.info.NativePrinterMap;
import qz.utils.SystemUtilities;
Expand All @@ -28,14 +29,28 @@
public class PrintServiceMatcher {
private static final Logger log = LogManager.getLogger(PrintServiceMatcher.class);

// PrintService is slow in CUPS, use a cache instead per JDK-7001133
// TODO: Include JDK version test for caching when JDK-7001133 is fixed upstream
private static final boolean useCache = SystemUtilities.isUnix();

public static NativePrinterMap getNativePrinterList(boolean silent, boolean withAttributes) {
NativePrinterMap printers = NativePrinterMap.getInstance();
printers.putAll(PrintServiceLookup.lookupPrintServices(null, null));
printers.putAll(true, lookupPrintServices());
if (withAttributes) { printers.values().forEach(NativePrinter::getDriverAttributes); }
if (!silent) { log.debug("Found {} printers", printers.size()); }
return printers;
}

private static PrintService[] lookupPrintServices() {
return useCache ? CachedPrintServiceLookup.lookupPrintServices() :
PrintServiceLookup.lookupPrintServices(null, null);
}

private static PrintService lookupDefaultPrintService() {
return useCache ? CachedPrintServiceLookup.lookupDefaultPrintService() :
PrintServiceLookup.lookupDefaultPrintService();
}

public static NativePrinterMap getNativePrinterList(boolean silent) {
return getNativePrinterList(silent, false);
}
Expand All @@ -45,15 +60,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)) {
printers.putAll(defaultService);
printers.putAll(false, defaultService);
}

return printers.get(defaultService);
Expand Down Expand Up @@ -81,6 +96,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 +170,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
133 changes: 133 additions & 0 deletions src/qz/printer/info/CachedPrintService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
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;

/**
* <code>PrintService.getName()</code> is slow, and gets increasingly slower the more times it's called.
*
* By overriding and caching the <code>PrintService</code> attributes, we're able to help suppress/delay the
* performance loss of this bug.
*
* See also JDK-7001133
*/
public class CachedPrintService implements PrintService {
private final PrintService printService;
private final long lifespan;
private final CachedObject<String> cachedName;
private final CachedObject<PrintServiceAttributeSet> cachedAttributeSet;
private final HashMap<Class<?>, CachedObject<?>> cachedAttributes = new HashMap<>();

public CachedPrintService(PrintService printService, long lifespan) {
this.printService = printService;
this.lifespan = lifespan;
cachedName = new CachedObject<>(this.printService::getName, lifespan);
cachedAttributeSet = new CachedObject<>(this.printService::getAttributes, lifespan);
}

public CachedPrintService(PrintService printService) {
this(printService, CachedObject.DEFAULT_LIFESPAN);
}

@Override
public String getName() {
return cachedName.get();
}

@Override
public DocPrintJob createPrintJob() {
return printService.createPrintJob();
}

@Override
public void addPrintServiceAttributeListener(PrintServiceAttributeListener listener) {
printService.addPrintServiceAttributeListener(listener);
}

@Override
public void removePrintServiceAttributeListener(PrintServiceAttributeListener listener) {
printService.removePrintServiceAttributeListener(listener);
}

@Override
public PrintServiceAttributeSet getAttributes() {
return cachedAttributeSet.get();
}

public PrintService getJavaxPrintService() {
return printService;
}

@Override
public <T extends PrintServiceAttribute> T getAttribute(Class<T> category) {
if (!cachedAttributes.containsKey(category)) {
Supplier<T> supplier = () -> printService.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 printService.getSupportedDocFlavors();
}

@Override
public boolean isDocFlavorSupported(DocFlavor flavor) {
return printService.isDocFlavorSupported(flavor);
}

@Override
public Class<?>[] getSupportedAttributeCategories() {
return printService.getSupportedAttributeCategories();
}

@Override
public boolean isAttributeCategorySupported(Class<? extends Attribute> category) {
return printService.isAttributeCategorySupported(category);
}

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

@Override
public Object getSupportedAttributeValues(Class<? extends Attribute> category, DocFlavor flavor, AttributeSet attributes) {
return printService.getSupportedAttributeValues(category, flavor, attributes);
}

@Override
public boolean isAttributeValueSupported(Attribute attrval, DocFlavor flavor, AttributeSet attributes) {
return printService.isAttributeValueSupported(attrval, flavor, attributes);
}

@Override
public AttributeSet getUnsupportedAttributes(DocFlavor flavor, AttributeSet attributes) {
return printService.getUnsupportedAttributes(flavor, attributes);
}

@Override
public ServiceUIFactory getServiceUIFactory() {
return printService.getServiceUIFactory();
}

public boolean equals(Object obj) {
return (obj == this ||
(obj instanceof PrintService &&
((PrintService)obj).getName().equals(getName())));
}

public int hashCode() {
return this.getClass().hashCode()+getName().hashCode();
}
}
81 changes: 81 additions & 0 deletions src/qz/printer/info/CachedPrintServiceLookup.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package qz.printer.info;

import qz.common.CachedObject;

import javax.print.PrintService;
import javax.print.PrintServiceLookup;

/**
* PrintService[] cache to workaround JDK-7001133
*
* See also <code>CachedPrintService</code>
*/
public class CachedPrintServiceLookup {
private static final CachedObject<CachedPrintService> cachedDefault = new CachedObject<>(CachedPrintServiceLookup::wrapDefaultPrintService);
private static final CachedObject<CachedPrintService[]> cachedPrintServices = new CachedObject<>(CachedPrintServiceLookup::wrapPrintServices);

// Keep CachedPrintService object references between calls to supplier
private static CachedPrintService[] cachedPrintServicesCopy = {};

static {
setLifespan(CachedObject.DEFAULT_LIFESPAN);
}

public static PrintService lookupDefaultPrintService() {
return cachedDefault.get();
}

public static void setLifespan(long lifespan) {
cachedDefault.setLifespan(lifespan);
cachedPrintServices.setLifespan(lifespan);
}

public static PrintService[] lookupPrintServices() {
return cachedPrintServices.get();
}

private static CachedPrintService wrapDefaultPrintService() {
PrintService javaxPrintService = PrintServiceLookup.lookupDefaultPrintService();
// CachedObject's supplier returns null
if(javaxPrintService == null) {
return null;
}
// If this CachedPrintService already exists, reuse it rather than wrapping a new one
CachedPrintService cachedPrintService = getMatch(cachedPrintServicesCopy, javaxPrintService);
if (cachedPrintService == null) {
// Wrap a new one
cachedPrintService = new CachedPrintService(javaxPrintService);
}
return cachedPrintService;
}

private static CachedPrintService[] wrapPrintServices() {
PrintService[] javaxPrintServices = PrintServiceLookup.lookupPrintServices(null, null);
CachedPrintService[] cachedPrintServices = new CachedPrintService[javaxPrintServices.length];
for (int i = 0; i < javaxPrintServices.length; i++) {
// If this CachedPrintService already exists, reuse it rather than wrapping a new one
cachedPrintServices[i] = getMatch(cachedPrintServicesCopy, javaxPrintServices[i]);
if (cachedPrintServices[i] == null) {
cachedPrintServices[i] = new CachedPrintService(javaxPrintServices[i]);
}
}
cachedPrintServicesCopy = cachedPrintServices;
// Immediately invalidate the defaultPrinter cache
cachedDefault.get(true);

return cachedPrintServices;
}

private static CachedPrintService getMatch(CachedPrintService[] array, PrintService javaxPrintService) {
if(array != null) {
for(CachedPrintService cps : array) {
// Note: Order of operations can cause the defaultService pointer to differ if lookupDefaultPrintService()
// is called before lookupPrintServices(...) because the provider will invalidate on refreshServices() if
// "sun.java2d.print.polling" is set to "false". We're OK with this because worst case, we just
// call "lpstat" a second time.
if (cps.getJavaxPrintService() == javaxPrintService) return cps;
}
}
return null;
}
}
4 changes: 2 additions & 2 deletions src/qz/printer/info/CupsPrinterMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ public class CupsPrinterMap extends NativePrinterMap {
private static final Logger log = LogManager.getLogger(CupsPrinterMap.class);
private Map<NativePrinter, List<PrinterResolution>> resolutionMap = new HashMap<>();

public synchronized NativePrinterMap putAll(PrintService... services) {
ArrayList<PrintService> missing = findMissing(services);
public synchronized NativePrinterMap putAll(boolean exhaustive, PrintService... services) {
ArrayList<PrintService> missing = findMissing(exhaustive, services);
if (missing.isEmpty()) { return this; }

String output = "\n" + ShellUtilities.executeRaw(new String[] {"lpstat", "-l", "-p"});
Expand Down
Loading

0 comments on commit 186ac38

Please sign in to comment.