From 6d974216fce112a68ccf4276130c458b4577a003 Mon Sep 17 00:00:00 2001
From: Radek Oslejsek <oslejsek@fi.muni.cz>
Date: Tue, 27 Apr 2021 11:43:44 +0200
Subject: [PATCH] Added HumanFaceCache implementations

---
 .../cz/fidentis/analyst/face/HumanFace.java   |  57 +++--
 .../fidentis/analyst/face/HumanFaceCache.java | 195 ++++++++++++++++++
 .../face/HumanFacePrivilegedCache.java        |  68 ++++++
 .../analyst/tests/EfficiencyTests.java        |  10 +-
 4 files changed, 309 insertions(+), 21 deletions(-)
 create mode 100644 Comparison/src/main/java/cz/fidentis/analyst/face/HumanFaceCache.java
 create mode 100644 Comparison/src/main/java/cz/fidentis/analyst/face/HumanFacePrivilegedCache.java

diff --git a/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFace.java b/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFace.java
index 17f82854..8e30f664 100644
--- a/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFace.java
+++ b/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFace.java
@@ -12,10 +12,10 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.InputStream;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 import java.io.Serializable;
+import java.util.Objects;
 
 /**
  * This class encapsulates data for a 3D scan of a single human face.
@@ -40,13 +40,15 @@ import java.io.Serializable;
  */
 public class HumanFace implements MeshListener, Serializable {
     
-    private MeshModel meshModel;
+    private final MeshModel meshModel;
 
     private Plane symmetryPlane;
 
     private MeshFacet cuttingPlane;
     
-    private transient EventBus eventBus = new EventBus();
+    private final transient EventBus eventBus = new EventBus();
+    
+    private final String id;
     
     /**
      * Reads a 3D human face from the given OBJ file.
@@ -56,20 +58,10 @@ public class HumanFace implements MeshListener, Serializable {
      * @throws IOException on I/O failure
      */
     public HumanFace(File file) throws IOException {
-        this(new FileInputStream(file));
-    }
-    
-    /**
-     * Reads a 3D human face from the given OBJ stream.
-     * Use {@link restoreFromFile} to restore the human face from a dump file.
-     * 
-     * @param is input stream with OBJ data
-     * @throws IOException on I/O failure
-     */
-    public HumanFace(InputStream is) throws IOException {
-        meshModel = MeshObjLoader.read(is);
+        meshModel = MeshObjLoader.read(new FileInputStream(file));
         meshModel.simplifyModel();
         meshModel.registerListener(this);
+        this.id = file.getCanonicalPath();
     }
     
     /**
@@ -142,6 +134,15 @@ public class HumanFace implements MeshListener, Serializable {
     public MeshFacet getCuttingPlane() {
         return cuttingPlane;
     }
+    
+    /**
+     * Return unique ID of the face.
+     * 
+     * @return unique ID of the face. 
+     */
+    public String getId() {
+        return this.id;
+    }
 
     /**
      * Creates serialized dump of the human face. Event buses are not stored.
@@ -172,4 +173,30 @@ public class HumanFace implements MeshListener, Serializable {
             return (HumanFace) fos.readObject();
         }
     }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final HumanFace other = (HumanFace) obj;
+        if (!Objects.equals(this.id, other.id)) {
+            return false;
+        }
+        return true;
+    }
+    
+    
 }
diff --git a/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFaceCache.java b/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFaceCache.java
new file mode 100644
index 00000000..188e8eee
--- /dev/null
+++ b/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFaceCache.java
@@ -0,0 +1,195 @@
+package cz.fidentis.analyst.face;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Singleton flyweight factory that creates and caches human faces. Faces are
+ * stored in the memory until there is enough space in the Java heap. Then they 
+ * are cached on disk automatically. The dumping strategy is directed by sub-classes
+ * of this abstract class.
+ * <p>
+ * Dumping faces on disk and their recovery is time consuming (we use serialization
+ * of all data structure, i.e., triangular mesh, k-d tree, etc.). It can be faster
+ * to re-load the face from OBJ file again and re-compute the data structures from scratch.
+ * </p>
+ * <p>
+ * Currently, listeners registered to {@code HumanFace} and {@code KdTree} 
+ * are neither dumped nor reconstructed!
+ * </p>
+ * 
+ * @author Radek Oslejsek
+ */
+public abstract class HumanFaceCache {
+    
+    /**
+     * Keep at least this portion of the Java heap memory free
+     */
+    public static double MIN_FREE_MEMORY = 0.05; // 5%
+    
+    private final Map<String, HumanFace> inMemoryFaces = new HashMap<>();
+    private final Map<String, File> dumpedFaces = new HashMap<>();
+    
+    /**
+     * Private constructor. Use {@link instance} instead to get and access the instance.
+     */
+    protected HumanFaceCache() {
+    }
+    
+    /**
+     * Loads new face. If the face is already loaded, then the existing instance
+     * is returned. 
+     * 
+     * @param file OBJ file with human face.
+     * @return Human face or {@code null}
+     */
+    public HumanFace loadFace(File file) {
+        try {
+            String faceId = file.getCanonicalPath();
+            
+            // In memory face:
+            HumanFace face = inMemoryFaces.get(faceId);
+            if (face != null) {
+                return face;
+            }
+            
+            // Dumped face:
+            face = recoverFace(faceId);
+            if (face != null) {
+                return face;
+            }
+            
+            // New face:
+            return storeNewFace(file);
+        } catch (IOException|ClassNotFoundException ex) {
+            return null;
+        }
+    }
+    
+    /**
+     * Returns a human face. Recovers the face from a dump file if necessary.
+     * 
+     * @param faceId ID of the face
+     * @return Human face or {@code null}
+     */
+    public HumanFace getFace(String faceId) {
+        try {
+            // In memory face:
+            HumanFace face = inMemoryFaces.get(faceId);
+            if (face != null) {
+                return face;
+            }
+            
+            // Dumped face:
+            face = recoverFace(faceId);
+            if (face != null) {
+                return face;
+            }
+            
+            // New face:
+            return null;
+        } catch (IOException|ClassNotFoundException ex) {
+            return null;
+        }
+    }
+    
+    /**
+     * Removed the face from the cache.
+     * 
+     * @param faceId Face ID
+     */
+    public void removeFace(String faceId) {
+        if (inMemoryFaces.remove(faceId) == null) {
+            dumpedFaces.remove(faceId);
+        }
+    }
+    
+    @Override
+    public String toString() {
+        StringBuilder ret = new StringBuilder();
+        ret.append(formatSize(Runtime.getRuntime().freeMemory())).
+                append(" free memory out of ").
+                append(formatSize(Runtime.getRuntime().maxMemory()));
+        ret.append(". In memory: ").
+                append(this.inMemoryFaces.size()).
+                append(", dumped: ").
+                append(this.dumpedFaces.size());
+        return ret.toString();
+    }
+    
+    /**
+     * Recovers the face from the dump file. Heap memory is released (some existing 
+     * face is dumped) if necessary. 
+     * 
+     * @param faceId Face ID
+     * @return Recovered face or {@code null} if the face was not dumped.
+     * @throws IOException on I/O error
+     * @throws ClassNotFoundException on I/O error
+     */
+    protected HumanFace recoverFace(String faceId) throws IOException, ClassNotFoundException {
+        File dumpFile = dumpedFaces.get(faceId);
+        if (dumpFile == null) {
+            return null;
+        }
+        freeMemory();
+        HumanFace face = HumanFace.restoreFromFile(dumpFile);
+        inMemoryFaces.put(faceId, face);
+        dumpedFaces.remove(faceId);
+        return face;
+    }
+    
+    /**
+     * Instantiates new face. Heap memory is released (some existing face is dumped) if necessary. 
+     * 
+     * @param file OBJ file
+     * @return Face instance.
+     * @throws IOException on I/O error
+     */
+    protected HumanFace storeNewFace(File file) throws IOException {
+        HumanFace face = new HumanFace(file);
+        freeMemory();
+        inMemoryFaces.put(face.getId(), face);
+        //inMemoryFaces.put(Long.toHexString(Double.doubleToLongBits(Math.random())), face);
+        return face;
+    }
+    
+    /**
+     * Checks and releases the heap, if necessary.
+     * 
+     * @return true if some existing face has been dumped to free the memory.
+     * @throws IOException on I/O error
+     */
+    protected boolean freeMemory() throws IOException {
+        double ratio = (double) Runtime.getRuntime().freeMemory() / Runtime.getRuntime().maxMemory();
+        if (ratio > MIN_FREE_MEMORY) {
+            return false;
+        }
+        String faceId = selectFaceForDump();
+        HumanFace face = inMemoryFaces.remove(faceId);
+        dumpedFaces.put(faceId, face.dumpToFile());
+        return true;
+    }
+    
+    /**
+     * Selects a face that will be dumped to file.
+     * 
+     * @return ID of the face that will be dumped to file.
+     */
+    protected abstract String selectFaceForDump();
+    
+    protected static String formatSize(long v) {
+        if (v < 1024) return v + " B";
+        int z = (63 - Long.numberOfLeadingZeros(v)) / 10;
+        return String.format("%.1f %sB", (double)v / (1L << (z*10)), " KMGTPE".charAt(z));
+    }
+    
+    protected Map<String, HumanFace> getInMemoryFaces() {
+        return this.inMemoryFaces;
+    }
+    
+    protected Map<String, File> getDumpedFaces() {
+        return this.dumpedFaces;
+    }
+}
diff --git a/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFacePrivilegedCache.java b/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFacePrivilegedCache.java
new file mode 100644
index 00000000..38df6205
--- /dev/null
+++ b/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFacePrivilegedCache.java
@@ -0,0 +1,68 @@
+package cz.fidentis.analyst.face;
+
+import java.io.IOException;
+
+/**
+ * This cache preserves the first X faces (privileged faces) always in the memory. 
+ * Faces that are loaded when there is not enough free memory are always dumped
+ * to file and only one of them is stored in the memory when used.
+ * 
+ * @author Radek Oslejsek
+ */
+public class HumanFacePrivilegedCache extends HumanFaceCache {
+    
+    private static HumanFacePrivilegedCache factory;
+    
+    /**
+     * The only one dumped face currently in the memory
+     */
+    private String recoveredFace;
+    
+    /**
+     * Private constructor. Use {@link instance} instead to get and access the instance.
+     */
+    protected HumanFacePrivilegedCache() {
+    }
+    
+    /**
+     * Returns the factory singleton instance.
+     * 
+     * @return the factory singleton instance
+     */
+    public static HumanFacePrivilegedCache instance() {
+        if (factory == null) {
+            factory = new HumanFacePrivilegedCache();
+        }
+        return factory;
+    }
+
+    @Override
+    protected String selectFaceForDump() {
+        if (this.recoveredFace != null) {
+            return this.recoveredFace;
+        } else {
+            // This should not happen. But return any face from the memory, anyway.
+            for (String faceId: getInMemoryFaces().keySet()) {
+                return faceId; 
+            }
+        }
+        return null;
+    }
+    
+    @Override
+    public void removeFace(String faceId) {
+        if (faceId.equals(this.recoveredFace)) {
+            this.recoveredFace = null;
+        }
+        super.removeFace(faceId);
+    }
+    
+    @Override
+    protected HumanFace recoverFace(String faceId) throws IOException, ClassNotFoundException {
+        HumanFace face = super.recoverFace(faceId);
+        if (face != null) {
+            this.recoveredFace = face.getId();
+        }
+        return face;
+    }
+}
diff --git a/GUI/src/main/java/cz/fidentis/analyst/tests/EfficiencyTests.java b/GUI/src/main/java/cz/fidentis/analyst/tests/EfficiencyTests.java
index d92797ad..410e864d 100644
--- a/GUI/src/main/java/cz/fidentis/analyst/tests/EfficiencyTests.java
+++ b/GUI/src/main/java/cz/fidentis/analyst/tests/EfficiencyTests.java
@@ -1,6 +1,7 @@
 package cz.fidentis.analyst.tests;
 
 import cz.fidentis.analyst.face.HumanFace;
+import cz.fidentis.analyst.face.HumanFacePrivilegedCache;
 import cz.fidentis.analyst.icp.Icp;
 import cz.fidentis.analyst.kdtree.KdTree;
 import cz.fidentis.analyst.mesh.core.MeshFacet;
@@ -44,14 +45,11 @@ public class EfficiencyTests {
         face1.getMeshModel().simplifyModel();
         face2.getMeshModel().simplifyModel();
         
-        List<HumanFace> list = new ArrayList<>();
-        for (int i = 0; i < 500; i++) {
-            System.out.print(i+". " + formatSize(Runtime.getRuntime().freeMemory()) + "/" + formatSize(Runtime.getRuntime().maxMemory()) + " ");
-            HumanFace face = printFaceLoad(faceFile4);
+        for (int i = 0; i < 100; i++) {
+            HumanFace face = HumanFacePrivilegedCache.instance().loadFace(faceFile4);
             face.getMeshModel().simplifyModel();
-            list.add(face);
+            System.out.println(i+".\t" + HumanFacePrivilegedCache.instance());
         }
-        list.clear();
         
         boolean relativeDist = false;
         boolean printDetails = false;
-- 
GitLab