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