=== modified file 'src/com/goldencode/p2j/ui/FontTable.java'
--- old/src/com/goldencode/p2j/ui/FontTable.java	2022-04-30 16:05:04 +0000
+++ new/src/com/goldencode/p2j/ui/FontTable.java	2022-05-25 11:18:39 +0000
@@ -60,6 +60,7 @@
 ** 034 EVL 20220110 Adding support for symbolic font flag.
 ** 035 AL2 20220425 Parse and use the web text scale font specification.
 ** 036 AL2 20220430 Changed default value for webScale.
+** 037 TJD 20220525 Support for text-metrics cache controlled by server
 */
 
 /*
@@ -207,6 +208,9 @@
    /** The fallback font, in case the default fonts are not specified. */
    private static final String FALLBACK_DEFAULT_FONT = "System, size 10";
    
+   /** The name of the node containing the font metrics management configuration */
+   public static final String FONT_METRICS = "font-metrics";
+
    /** A map containing the legacy font metrics. */
    private static Map<String, LegacyFontMetrics> legacyFontMetrics = new HashMap<>();
    
@@ -215,6 +219,15 @@
    
    /** A map containing legacy text/font keys and legacy font width and height dimensions */
    private static Map<String, int[]> sharedLegacyTextDimensions = new HashMap<>();
+
+   /** A runtime cache for legacy text/font keys and legacy font width and height dimensions
+    * Cache entries are metrics of legacy font stored as int[2]
+    * Invalid entries are stored as int[0], so client dont need to query server for these
+    */
+   private static LRUCache<String, int[]> sharedLegacyTextDimensionsCache = new LRUCache<>(10);
+
+   /** Size of a runtime cache for legacy text/font keys and legacy font width and height dimensions */
+   private static int sharedLegacyTextDimensionsCacheSize=0;
    
    /** A set containing the name of the fixed fonts. */
    private static Set<String> fixedFonts = new TreeSet<>();
@@ -1263,6 +1276,17 @@
     */
    static int[] getLegacyTextMetrics(String text, String key)
    {
+      String cacheKey = buildTextMetricsKey(key,text);
+      
+      int [] textMetrics = null;
+      synchronized(sharedLegacyTextDimensionsCache) {
+         textMetrics=sharedLegacyTextDimensionsCache.get(cacheKey);
+      }
+      if (textMetrics!=null)
+      {
+         return textMetrics;
+      }
+      
       LegacyTextMetrics ltm = legacyTextMetrics.get(text);
       if (ltm == null)
       {
@@ -1270,7 +1294,11 @@
          {
             LOG.fine("Text [" + text+ "] has no legacy metrics with font [" + key + "].");
          }
-
+         
+         synchronized(sharedLegacyTextDimensionsCache) {
+            // Store invalid metrics value, so client knows it should not query server for this
+            sharedLegacyTextDimensionsCache.putIfAbsent(cacheKey,new int[0]);
+         }
          return null;
       }
       
@@ -1281,14 +1309,33 @@
          {
             LOG.fine("Text [" + text+ "] has no legacy metrics with font [" + key + "].");
          }
-
+         synchronized(sharedLegacyTextDimensionsCache) {
+            // Store invalid metrics value, so client knows it should not query server for this
+            sharedLegacyTextDimensionsCache.putIfAbsent(cacheKey,new int[0]);
+         }
          return null;
       }
       
-      return new int[] { lfm.width, lfm.height };
+      textMetrics = new int[] { lfm.width, lfm.height };
+      synchronized(sharedLegacyTextDimensionsCache) {
+         // Cache is based on HashMap so null values are allowed
+         sharedLegacyTextDimensionsCache.putIfAbsent(cacheKey,textMetrics);
+      }
+      return textMetrics;
    }
    
    /**
+    * buildTextMetricsKey - a helper method to unify TextMetrics keys creation
+    * @param key - font key
+    * @param text - text
+    * @return textMetrics key
+    */
+   private static String buildTextMetricsKey(String key, String text)
+   {
+      return key + "#" + text;
+   }
+
+   /**
     * Get the legacy text dimensions for all the texts and each font defined in the font table,
     * shared among all users.
     *
@@ -1297,6 +1344,90 @@
     */
    static Map<String, int[]> readLegacyTextMetrics()
    {
+      DirectoryService ds = DirectoryService.getInstance();
+      
+      if (!ds.bind())
+      {
+         if (LOG.isLoggable(Level.SEVERE))
+         {
+            LOG.severe("FontTable.readLegacyTextMetrics - could not bind directory.");
+         }
+         
+         return sharedLegacyTextDimensions;
+      }
+      
+      try
+      {
+         // these can be set per-user
+         String path = Utils.findDirectoryNodePath(ds, FONT_METRICS + "/cache-size", "string", 
+                                                   DirScope.BOTH, null);
+         if (path == null)
+         {
+            if (LOG.isLoggable(Level.INFO))
+            {
+               LOG.info("No font metrics are defined in the directory.");
+            }
+            
+            return sharedLegacyTextDimensions;
+         }
+         String cacheSizeString = ds.getNodeString(path,"value");
+         
+         if (cacheSizeString==null)
+         {
+            if (LOG.isLoggable(Level.INFO))
+            {
+               LOG.info("No cache size is defined in the directory.");
+               return sharedLegacyTextDimensions;
+            }
+         }
+         if ("FULL".equals(cacheSizeString.toUpperCase()) || "ALL".equals(cacheSizeString.toUpperCase()))
+         {
+            return sharedLegacyTextDimensions;
+         }
+         int requestedCacheSize=Integer.valueOf(cacheSizeString);
+         // rebuild cache only if cache size has changed
+         if (requestedCacheSize!=sharedLegacyTextDimensionsCacheSize)
+         {
+               sharedLegacyTextDimensionsCache=new LRUCache<>(requestedCacheSize);
+               sharedLegacyTextDimensionsCacheSize=requestedCacheSize;
+         }
+         // convert cache to Map<> and send back
+         HashMap<String, int[]> trimmedTextDimensions=new HashMap<>(sharedLegacyTextDimensionsCache.size());
+         for(String key:sharedLegacyTextDimensionsCache.keys())
+         {
+            trimmedTextDimensions.put(key, sharedLegacyTextDimensionsCache.get(key));
+         }
+         
+         // send configured cache size to client
+         if (trimmedTextDimensions.size()<requestedCacheSize)
+         {
+            // fill up cache with invalid entries, so remote side always gets a map of a proper size
+            int i=0;
+            while(trimmedTextDimensions.size()<requestedCacheSize)
+            {
+               trimmedTextDimensions.put(String.valueOf(i++), new int[0]);
+            }
+         }
+         return trimmedTextDimensions;
+      }
+      catch (NumberFormatException e)
+      {
+         if (LOG.isLoggable(Level.INFO))
+         {
+            LOG.info("Invalid cache size is defined in the directory: not an integer.");
+         }
+      }
+      finally
+      {
+         if (!ds.unbind())
+         {
+            if (LOG.isLoggable(Level.SEVERE))
+            {
+               LOG.severe("FontTable.readFontAliases - could not unbind directory.");
+            }
+         }
+      }
+
       return sharedLegacyTextDimensions;
    }
    
@@ -1573,7 +1704,7 @@
          {
             String fontKey = fontEntry.getKey();
             LegacyFontMetrics fontMx = fontEntry.getValue();
-            String textKey = fontKey + "#" + text;
+            String textKey = buildTextMetricsKey(fontKey,text);
             
             dimensions.put(textKey, new int[] { fontMx.width, fontMx.height });
          }

=== modified file 'src/com/goldencode/p2j/ui/client/FontManager.java'
--- old/src/com/goldencode/p2j/ui/client/FontManager.java	2022-05-03 10:23:10 +0000
+++ new/src/com/goldencode/p2j/ui/client/FontManager.java	2022-05-25 11:19:34 +0000
@@ -55,6 +55,7 @@
 ** 035 HC  20200416 Extended direct font control to allow to use system font names.
 ** 036 AL2 20220425 Added web text scale to font key generation.
 **                  Onyl web scaled fonts are checked to be locally installed.
+** 037 TJD 20220525 Support for text-metrics cache controlled by server
 */
 
 /*
@@ -115,6 +116,7 @@
 import java.util.*;
 import java.util.logging.*;
 
+import com.goldencode.cache.LRUCache;
 import com.goldencode.p2j.security.*;
 import com.goldencode.p2j.ui.*;
 import com.goldencode.p2j.ui.chui.*;
@@ -1359,19 +1361,25 @@
    {
       WorkArea wa = locate();
       String tkey = getTextKey(key, text);
-      int[] metrics = wa.textMetrics.get(tkey);
-      
-      if (metrics != null)
+      int[] metrics = null;
+      synchronized(wa.textMetrics){
+         metrics = wa.textMetrics.get(tkey);
+      }
+      // invalid metrics.length is 0
+      if (metrics != null && metrics.length > 0)
       {
          // found from cache, fast exit
          return metrics;
       }
-
-      // check server-side
-      metrics = wa.server.getLegacyTextMetrics(text, key);
       
+      // check server-side only if not in cache  
       if (metrics == null)
       {
+         metrics = wa.server.getLegacyTextMetrics(text, key);
+      }
+      
+      if (metrics == null || metrics.length == 0)
+      {
          // let the driver measure the string
          FontDetails<?> fd = wa.fontCache.get(key);
          
@@ -1397,8 +1405,9 @@
       }
       
       // cache it
-      wa.textMetrics.put(tkey, metrics);
-      
+      synchronized(wa.textMetrics){
+         wa.textMetrics.putIfAbsent(tkey, metrics);
+      }
       return metrics;
    }
 
@@ -1454,8 +1463,10 @@
       /** Access to the client's environment.  May be <code>null</code> if not on a Windows OS. */
       private EnvironmentDaemon ed;
       
-      /** A cache of text metrics.  0-index is the width, 1-index is the height. */
-      private final Map<String, int[]> textMetrics = new HashMap<String, int[]>();
+      /** A cache of text metrics.  0-index is the width, 1-index is the height.
+       *  Default size will help in case server will not send anything at startup
+       */
+      private LRUCache<String, int[]> textMetrics = new LRUCache<String, int[]>(100);
       
       /** Flag to track if the work area has been initialized. */
       private boolean initialized = false;
@@ -1497,7 +1508,6 @@
             fontCache.clear();
             windowFontTables.clear();
             envFontTables.clear();
-            textMetrics.clear();
             
             // initialize only in GUI mode
             this.server = server;
@@ -1523,8 +1533,16 @@
             
             // pre-populate the legacy text metrics for all the fonts in the font-table
             Map<String, int[]> legacyTextMetrics = server.readLegacyTextMetrics();
-            textMetrics.putAll(legacyTextMetrics);
 
+            // put text metrics from server into local cache
+            if (legacyTextMetrics!=null)
+            {
+               textMetrics=new LRUCache<>(legacyTextMetrics.size());
+               for(String key:legacyTextMetrics.keySet())
+               {
+                  textMetrics.put(key,legacyTextMetrics.get(key));
+               }
+            }
             // the rest of the fonts are installing here
             driver.releaseWindow();
 

