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

Printer Status: Performance improvements #1124

Merged
merged 15 commits into from
Oct 12, 2023
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
tresf marked this conversation as resolved.
Show resolved Hide resolved
*/
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
tresf marked this conversation as resolved.
Show resolved Hide resolved
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