Skip to content

"impossible" failure of a simple hash table #4399

@ddyer0

Description

@ddyer0

I maintain a simple hashtable mapping fonts to a companion fontmetrics object. Somehow,
this hashtable maps all fonts to the same slot. The only rational explanation for this would be
if "font" objects have a custom hashcode and equals function that are faulty.

This behavior is brand new, and affects IOS only. The underlying code has been in use for
years.

Here's a test program

/**
 * demo program for  issue xxx, fontmetric cache failure
*/
package com.boardspace.dtest;

import com.codename1.ui.Component;
// 
// drawing pdfs
//
//
import com.codename1.ui.Display;
import com.codename1.ui.Form;
import com.codename1.ui.Graphics;
import com.codename1.ui.Toolbar;
import com.codename1.ui.geom.Dimension;
import com.codename1.ui.plaf.UIManager;
import com.codename1.ui.util.Resources;

import java.util.ArrayList;

/*
Copyright 2006-2023 by Dave Dyer

This file is part of the Boardspace project.

Boardspace is free software: you can redistribute it and/or modify it under the terms of 
the GNU General Public License as published by the Free Software Foundation, 
either version 3 of the License, or (at your option) any later version.

Boardspace is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with Boardspace.
If not, see https://www.gnu.org/licenses/.
*/

import java.util.Hashtable;

import com.codename1.ui.Font;

import com.codename1.ui.geom.Rectangle2D;

//There are these fonts now: https://www.codenameone.com/blog/good-looking-by-default-native-fonts-simulator-detection-more.html
class LruCache
{	int size=0;
static int hits = 0;
static int probes = 0;
String strs[];
int values[];
public String toString() { return "<wc "+hits*100/probes+">"; }
static int NOVALUE = - 0x023536215;
LruCache(int sz)
{	size = sz;
	strs = new String[sz];
	values = new int[sz];
}

synchronized int getValue(String key)
{	probes++;
	for(int i=0;i<size; i++) 
		{ if(key==strs[i]) 
			{ hits++; 
			  int v = values[i];
			  while(i>0)
			  {
				  strs[i] = strs[i-i];
				  values[i] = values[i-1];
				  i--;
			  }
			return v; }
			}
	return NOVALUE;
}

synchronized void storeValue(String key,int val)
{	for(int idx=size-2; idx>=0;idx--)
	{	strs[idx+1] = strs[idx];
		values[idx+1] = values[idx];
	}
	values[0] = val;
	strs[0] = key;
}}

class FontMetrics {


private Font myFont;
static Hashtable <Font,FontMetrics>fmCache  = new Hashtable<Font,FontMetrics>();
LruCache widthCache = new LruCache(3);
public FontMetrics(Font f) 
	{ myFont = f;
	  int oldsize = fmCache.size();
	  FontMetrics old = fmCache.get(f);
	  fmCache.put(f,this); 
	  int newsize = fmCache.size();
	  Test.messages.add("add font "+f+" was "+old+" "+oldsize);
	  Test.messages.add("new "+this+newsize);

	}
public static FontMetrics getFontMetrics(Font g) 
{	
	FontMetrics m = fmCache.get(g);
	if(m==null) 
		{	
			m = new FontMetrics(g);
		}
	if(m.myFont!=g)
	{
		Test.messages.add("Impossible mismatch fm "+m+" "+m.myFont+" "+g);
		FontMetrics m2 = fmCache.get(m.myFont);
		Test.messages.add("Other is "+m2);
		m = new FontMetrics(g);
	}
	return m;
}
public int getMaxDescent() { return myFont.getDescent(); }

public static FontMetrics getFontMetrics(Graphics g) 
{ 	return(getFontMetrics(g.getFont())); 
}

public Rectangle2D getStringBounds(String str, Graphics context)
{	 
	return(new Rectangle2D(0,0,stringWidth(str),getHeight()));
}

public Rectangle2D getStringBounds(String str, int from,int to,Graphics context)
{
	return(new Rectangle2D(stringWidth(str.substring(0,from)),
			0,
			stringWidth(str.substring(from,to)),
			getHeight()));
}

public Font getFont() { return(myFont); }

public int stringWidth(String str) 
{ 	int w = widthCache.getValue(str);
	// widthcache per font is only intended to optimize when
	// the same string is queried multiple times while being prepped for display
	if(w!=LruCache.NOVALUE) { return w; }
	w = myFont.stringWidth(str);
	widthCache.storeValue(str,w);
	return(w); 
}
public int getHeight() 
{	
	int h = myFont.getHeight(); 
	return h;
}
public int getDescent() { return(myFont.getDescent()); }
public int getAscent() { return(myFont.getAscent()); }
public int getMaxAscent() 
	{ // the standard spec is a little fuzzy, it says
	  // "ascent" is the height for most alphanumeric characters,
	  // but some characters may be taller.  So we hope getAscent is 
	  // good enough
		return(myFont.getAscent());	
	}

public static  Font getFont(Font f,int size)
{	
	Font fd = f.derive(size,0);
	return(fd);
}
static int fontFaceCode(String spec)
{
	   if ("monospaced".equalsIgnoreCase(spec)) { return Font.FACE_MONOSPACE; }
	   if ("serif".equalsIgnoreCase(spec)) { return Font.FACE_PROPORTIONAL; }
	   return Font.FACE_SYSTEM;
}

public static Font getFont(String family,int size)
{	
	Font f = Font.createSystemFont(fontFaceCode(family),0,size);
	return f;
}
}


class Test extends Component implements Runnable
{	Form form = null;
	static ArrayList<String>messages = new ArrayList<String>();
	static void Message(String m) { messages.add(m); }
	
	public Component getCanvas() { return this; }
	Test(Form f)
	{	form = f;
		setPreferredSize(new Dimension(f.getWidth(),f.getHeight()));
	}
	int loops = 0;
	int phase = 0;
	String got = "first with coords";
	String got2 = "second with rectangle";	
	public void paint(Graphics gc)
	{	
		int w = form.getWidth();
		int h = form.getHeight();
		int step = 40;
		gc.setColor(0x806060);
		gc.fillRect(0,0,w,h);
		gc.setColor(0);
		String phase = "before";
		try {
			for(int i=0;i<messages.size() && i*step<h;i++ )
			{
				gc.drawString(messages.get(i),20,(i+1)*step);
			}
		}
		catch(Throwable err)
		{
			gc.drawString("error "+err+" in phase "+phase,100,400);
		}
	}
	public void run() {
		int sz = 10;
		Font f = FontMetrics.getFont("Arial Unicode MS",sz);
		for(int i=0;i<10;i++)
		{
			FontMetrics.getFontMetrics(f);
			Font f1 = FontMetrics.getFont(f,sz++);
			FontMetrics.getFontMetrics(f1);
		}

		while(true)
			{ this.repaint();
			  try { Thread.sleep(1000); }
			  catch(InterruptedException e) {}

			}
	}
}
@SuppressWarnings("rawtypes")
public class Dtest{

private Form current;
@SuppressWarnings("unused")
private Resources theme;



public void init(Object context) {
    theme = UIManager.initFirstTheme("/theme");
    // Pro only feature, uncomment if you have a pro subscription
    // Log.bindCrashProtection(true);
    

}
public Toolbar toolbar = null;


public void run()
{
    

}
public void start() {
    
    if(current != null){
        current.show();
    }
    Form hi = new Form();
    hi.setToolbar(new Toolbar());
    Test can = new Test(hi);
    hi.addComponent(can);
    can.setVisible(true);
    new Thread(can).start();
    hi.show();

}
public void stop() {
    current = Display.getInstance().getCurrent();
}

public void destroy() {
}

}

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions