Fix filesystem caching performance as in bug #10318
authorEd Swartz <ed.swartz@nokia.com>
Tue, 05 Jan 2010 11:23:50 -0600
changeset 743 78fd666a897a
parent 742 9fca333f8b5e
child 745 51d63d83aad1
Fix filesystem caching performance as in bug #10318
builder/com.nokia.carbide.cdt.builder/src/com/nokia/carbide/cdt/builder/DefaultIncludeFileLocator.java
builder/com.nokia.carbide.cdt.builder/src/com/nokia/carbide/cdt/builder/EpocEngineHelper.java
builder/com.nokia.carbide.cdt.builder/src/com/nokia/carbide/cdt/internal/builder/CarbideBuildManager.java
builder/com.nokia.carbide.cdt.builder/src/com/nokia/carbide/cdt/internal/builder/CarbideProjectModifier.java
core/com.nokia.carbide.cpp.sdk.core/src/com/nokia/carbide/cpp/internal/api/sdk/SymbianBuildContext.java
core/com.nokia.carbide.cpp.sdk.core/src/com/nokia/carbide/cpp/internal/api/sdk/SymbianBuildContextDataCache.java
core/com.nokia.carbide.cpp.sdk.core/src/com/nokia/carbide/cpp/internal/sdk/core/model/AbstractSDKManager.java
core/com.nokia.carbide.cpp.sdk.ui/src/com/nokia/carbide/cpp/internal/sdk/ui/SDKPreferencePage.java
core/com.nokia.cpp.utils.core/src/com/nokia/cpp/internal/api/utils/core/ExternalFileInfoCache.java
core/com.nokia.cpp.utils.core/src/com/nokia/cpp/internal/api/utils/core/ExternalFileInfoCollection.java
core/com.nokia.cpp.utils.core/src/com/nokia/cpp/internal/api/utils/core/Tuple.java
project/com.nokia.carbide.cpp.epoc.engine/src/com/nokia/carbide/cpp/epoc/engine/EpocEnginePlugin.java
project/com.nokia.carbide.cpp.epoc.engine/src/com/nokia/carbide/cpp/epoc/engine/preprocessor/DefaultTranslationUnitProvider.java
project/com.nokia.carbide.cpp.epoc.engine/src/com/nokia/carbide/cpp/epoc/engine/preprocessor/IDefine.java
project/com.nokia.carbide.cpp.epoc.engine/src/com/nokia/carbide/internal/api/cpp/epoc/engine/preprocessor/MacroScanner.java
project/com.nokia.carbide.cpp.epoc.engine/src/com/nokia/carbide/internal/cpp/epoc/engine/model/ViewDataCache.java
--- a/builder/com.nokia.carbide.cdt.builder/src/com/nokia/carbide/cdt/builder/DefaultIncludeFileLocator.java	Tue Jan 05 11:22:47 2010 -0600
+++ b/builder/com.nokia.carbide.cdt.builder/src/com/nokia/carbide/cdt/builder/DefaultIncludeFileLocator.java	Tue Jan 05 11:23:50 2010 -0600
@@ -23,12 +23,12 @@
 import java.util.*;
 
 import org.eclipse.core.resources.IProject;
-import org.eclipse.core.runtime.IPath;
 
 import com.nokia.carbide.cdt.builder.project.ICarbideProjectInfo;
-import com.nokia.carbide.cpp.epoc.engine.model.sbv.ISBVView;
+import com.nokia.carbide.cpp.internal.api.sdk.SymbianBuildContext;
 import com.nokia.carbide.cpp.sdk.core.*;
 import com.nokia.carbide.internal.api.cpp.epoc.engine.preprocessor.BasicIncludeFileLocator;
+import com.nokia.cpp.internal.api.utils.core.Check;
 
 public class DefaultIncludeFileLocator extends BasicIncludeFileLocator {
 	/**
@@ -40,7 +40,6 @@
 	 */
 	public DefaultIncludeFileLocator(IProject project, ISymbianBuildContext buildContext) {
 		super(null, null);
-		
 		List<File> systemPaths = new ArrayList<File>();
 		if (buildContext != null && buildContext.getSDK() != null) {
 			// search implicit bld.inf directory if known
@@ -51,84 +50,11 @@
 					systemPaths.add(cpi.getAbsoluteBldInfPath().removeLastSegments(1).toFile());
 				}
 			}
+
+			// get info from context
+			Check.checkState(buildContext instanceof SymbianBuildContext);
 			
-			IBSFPlatform bsfplatform = buildContext.getSDK().getBSFCatalog().findPlatform(buildContext.getPlatformString());
-			ISBVPlatform sbvPlatform = buildContext.getSDK().getSBVCatalog().findPlatform(buildContext.getPlatformString());
-
-			// look in the epoc32 directory of the SDK
-			IPath includePath = buildContext.getSDK().getIncludePath();
-			if (includePath != null) {
-				File includeDir = includePath.toFile().getAbsoluteFile();
-				File dir;
-				
-				// get additional include directories from BSF platform, if defined
-				if (bsfplatform != null) {
-					IPath[] systemIncludePaths = bsfplatform.getSystemIncludePaths();
-					for (IPath path : systemIncludePaths) {
-						dir = path.toFile();
-						if (dir.exists() && dir.isDirectory()) {
-							systemPaths.add(dir);
-						}
-					}
-				} else if (sbvPlatform != null){
-					
-					LinkedHashMap<IPath, String> platPaths = sbvPlatform.getBuildIncludePaths();
-					Set<IPath> set = platPaths.keySet();
-					for (IPath path : set) {
-						String pathType = platPaths.get(path);
-						if (pathType.equalsIgnoreCase(ISBVView.INCLUDE_FLAG_PREPEND) || pathType.equalsIgnoreCase(ISBVView.INCLUDE_FLAG_SET)){
-							dir = path.toFile();
-							systemPaths.add(dir);
-						}
-					}
-				}
-				else {
-					// legacy behavior 
-					if (buildContext.getPlatformString().equals(ISymbianBuildContext.EMULATOR_PLATFORM)) {
-						dir = new File(includeDir, "wins"); //$NON-NLS-1$
-						if (dir.exists() && dir.isDirectory()) {
-							systemPaths.add(dir);
-						}
-					}
-				}
-
-				// add OEM dir
-				dir = new File(includeDir, "oem"); //$NON-NLS-1$
-				if (dir.exists() && dir.isDirectory()) {
-					systemPaths.add(dir);
-				}
-	
-				// and finally the normal include dir
-				systemPaths.add(includeDir);
-				
-				// and finally, finally, if this is an SBV add any paths with the append flag
-				if (sbvPlatform != null){
-					
-					Map<IPath, String> platPaths = sbvPlatform.getBuildIncludePaths();
-					Set<IPath> set = platPaths.keySet();
-					for (IPath path : set) {
-						String pathType = platPaths.get(path);
-						if (pathType.equalsIgnoreCase(ISBVView.INCLUDE_FLAG_APPEND)){
-							dir = path.toFile();
-							systemPaths.add(dir);
-						}
-					}	
-				}
-			}
-			
-			// also search files in same folder as variant.hrh
-			File prefix = buildContext.getSDK().getPrefixFile();
-			if (sbvPlatform != null){
-				// might be an alternate HRH file to use
-				IPath varVarHRH = sbvPlatform.getBuildVariantHRHFile();
-				if (!varVarHRH.toFile().equals(prefix) && varVarHRH.toFile().exists()){
-					prefix = varVarHRH.toFile();
-				} 
-			}
-			if (prefix != null) {
-				systemPaths.add(prefix.getParentFile());
-			}
-
+			systemPaths.addAll(((SymbianBuildContext) buildContext).getCachedSystemIncludePaths());
 		}
 		setPaths(null, (File[]) systemPaths.toArray(new File[systemPaths.size()]));
 	}
--- a/builder/com.nokia.carbide.cdt.builder/src/com/nokia/carbide/cdt/builder/EpocEngineHelper.java	Tue Jan 05 11:22:47 2010 -0600
+++ b/builder/com.nokia.carbide.cdt.builder/src/com/nokia/carbide/cdt/builder/EpocEngineHelper.java	Tue Jan 05 11:23:50 2010 -0600
@@ -27,6 +27,7 @@
 import com.nokia.carbide.cpp.epoc.engine.preprocessor.AcceptedNodesViewFilter;
 import com.nokia.carbide.cpp.epoc.engine.preprocessor.AllNodesViewFilter;
 import com.nokia.carbide.cpp.internal.api.sdk.SymbianBuildContext;
+import com.nokia.carbide.cpp.internal.api.sdk.SymbianBuildContextDataCache;
 import com.nokia.carbide.cpp.sdk.core.ISymbianBuildContext;
 import com.nokia.carbide.cpp.sdk.core.ISymbianSDK;
 import com.nokia.carbide.internal.api.cpp.epoc.engine.model.pkg.*;
@@ -88,35 +89,41 @@
 		
 		monitor.beginTask("Scanning bld.inf for mmp and make files", buildConfigs.size());
 
-		for (final ISymbianBuildContext context : buildConfigs) {
-			EpocEnginePlugin.runWithBldInfData(bldInfFilePath, 
-					new DefaultViewConfiguration(context, bldInfFilePath, new AcceptedNodesViewFilter()), 
-					new BldInfDataRunnableAdapter() {
-						public Object run(IBldInfData data) {
-							for (IMakMakeReference ref : data.getMakMakeReferences()) {
-								normalFiles.add(getFullPath(data, ref.getPath()));
-							}
-							for (IMakMakeReference ref : data.getTestMakMakeReferences()) {
-								boolean ignore = false;
-								for (String att : ref.getAttributes()) {
-									if (att.equalsIgnoreCase("ignore")) { //$NON-NLS-1$
-										ignore = true;
-										break;
+		try {
+			// let cache know we're iterating a lot
+			SymbianBuildContextDataCache.startProjectOperation();
+			
+			for (final ISymbianBuildContext context : buildConfigs) {
+				EpocEnginePlugin.runWithBldInfData(bldInfFilePath, 
+						new DefaultViewConfiguration(context, bldInfFilePath, new AcceptedNodesViewFilter()), 
+						new BldInfDataRunnableAdapter() {
+							public Object run(IBldInfData data) {
+								for (IMakMakeReference ref : data.getMakMakeReferences()) {
+									normalFiles.add(getFullPath(data, ref.getPath()));
+								}
+								for (IMakMakeReference ref : data.getTestMakMakeReferences()) {
+									boolean ignore = false;
+									for (String att : ref.getAttributes()) {
+										if (att.equalsIgnoreCase("ignore")) { //$NON-NLS-1$
+											ignore = true;
+											break;
+										}
+									}
+									if (!ignore) {
+										testFiles.add(getFullPath(data, ref.getPath()));
 									}
 								}
-								if (!ignore) {
-									testFiles.add(getFullPath(data, ref.getPath()));
-								}
+								return null;
 							}
-							return null;
-						}
-				});
-
-			monitor.worked(1);
+					});
+	
+				monitor.worked(1);
+			}
+				
+			monitor.done();
+		} finally {
+			SymbianBuildContextDataCache.endProjectOperation();
 		}
-		
-		monitor.done();
-		
 		for (IPath normalPath : normalFiles){
 			normalMakMakePaths.add(normalPath);
 		}
@@ -145,29 +152,35 @@
 		
 		monitor.beginTask("Scanning bld.inf project extensions", buildConfigs.size());
 
-		for (final ISymbianBuildContext context : buildConfigs) {
-			EpocEnginePlugin.runWithBldInfData(bldInfFilePath, 
-					new DefaultViewConfiguration(context, bldInfFilePath, new AcceptedNodesViewFilter()), 
-					new BldInfDataRunnableAdapter() {
-						public Object run(IBldInfData data) {
-							BldInfViewPathHelper helper = new BldInfViewPathHelper(data, context);
-							for (IExtension extension : data.getExtensions()) {
-								IPath extensionMakefileBase = helper.convertExtensionTemplateToFilesystem(extension.getTemplatePath());
-								normalFiles.add(extensionMakefileBase.addFileExtension("mk")); //$NON-NLS-1$
+		try {
+			// let cache know we're iterating a lot
+			SymbianBuildContextDataCache.startProjectOperation();
+			
+			for (final ISymbianBuildContext context : buildConfigs) {
+				EpocEnginePlugin.runWithBldInfData(bldInfFilePath, 
+						new DefaultViewConfiguration(context, bldInfFilePath, new AcceptedNodesViewFilter()), 
+						new BldInfDataRunnableAdapter() {
+							public Object run(IBldInfData data) {
+								BldInfViewPathHelper helper = new BldInfViewPathHelper(data, context);
+								for (IExtension extension : data.getExtensions()) {
+									IPath extensionMakefileBase = helper.convertExtensionTemplateToFilesystem(extension.getTemplatePath());
+									normalFiles.add(extensionMakefileBase.addFileExtension("mk")); //$NON-NLS-1$
+								}
+								for (IExtension extension : data.getTestExtensions()) {
+									IPath extensionMakefileBase = helper.convertExtensionTemplateToFilesystem(extension.getTemplatePath());
+									testFiles.add(extensionMakefileBase.addFileExtension("mk")); //$NON-NLS-1$
+								}
+								return null;
 							}
-							for (IExtension extension : data.getTestExtensions()) {
-								IPath extensionMakefileBase = helper.convertExtensionTemplateToFilesystem(extension.getTemplatePath());
-								testFiles.add(extensionMakefileBase.addFileExtension("mk")); //$NON-NLS-1$
-							}
-							return null;
-						}
-				});
-
-			monitor.worked(1);
+					});
+	
+				monitor.worked(1);
+			}
+		} finally {
+			SymbianBuildContextDataCache.endProjectOperation();
+			monitor.done();
 		}
 		
-		monitor.done();
-		
 		for (IPath normalPath : normalFiles){
 			normalExtensionPaths.add(normalPath);
 		}
@@ -195,30 +208,37 @@
 		
 		monitor.beginTask("Scanning bld.inf project extensions", buildConfigs.size());
 
-		for (final ISymbianBuildContext context : buildConfigs) {
-			EpocEnginePlugin.runWithBldInfData(bldInfFilePath, 
-					new DefaultViewConfiguration(context, bldInfFilePath, new AcceptedNodesViewFilter()), 
-					new BldInfDataRunnableAdapter() {
-						public Object run(IBldInfData data) {
-							for (IExtension extension : data.getExtensions()) {
-								if (extension.getName() != null) {
-									normalFiles.add(extension);
+		try {
+			// let cache know we're iterating a lot
+			SymbianBuildContextDataCache.startProjectOperation();
+			
+			for (final ISymbianBuildContext context : buildConfigs) {
+				EpocEnginePlugin.runWithBldInfData(bldInfFilePath, 
+						new DefaultViewConfiguration(context, bldInfFilePath, new AcceptedNodesViewFilter()), 
+						new BldInfDataRunnableAdapter() {
+							public Object run(IBldInfData data) {
+								for (IExtension extension : data.getExtensions()) {
+									if (extension.getName() != null) {
+										normalFiles.add(extension);
+									}
 								}
+								for (IExtension extension : data.getTestExtensions()) {
+									if (extension.getName() != null) {
+										testFiles.add(extension);
+									}
+								}
+								return null;
 							}
-							for (IExtension extension : data.getTestExtensions()) {
-								if (extension.getName() != null) {
-									testFiles.add(extension);
-								}
-							}
-							return null;
-						}
-				});
-
-			monitor.worked(1);
+					});
+	
+				monitor.worked(1);
+			}
+		} finally {		
+			monitor.done();
+			
+			SymbianBuildContextDataCache.endProjectOperation();
 		}
 		
-		monitor.done();
-		
 		for (IExtension normal : normalFiles){
 			normalExtensions.add(normal);
 		}
@@ -240,29 +260,36 @@
 
 		monitor.beginTask("Scanning bld.inf project extensions", buildConfigs.size());
 
-		for (final ISymbianBuildContext context : buildConfigs) {
-			EpocEnginePlugin.runWithBldInfData(bldInfFilePath, 
-					new DefaultViewConfiguration(context, bldInfFilePath, new AcceptedNodesViewFilter()), 
-					new BldInfDataRunnableAdapter() {
-						public Object run(IBldInfData data) {
-							for (IExtension extension : data.getExtensions()) {
-								if (extension.getName() == null) {
-									extensions.add(extension);
+		try {
+			// let cache know we're iterating a lot
+			SymbianBuildContextDataCache.startProjectOperation();
+			
+			for (final ISymbianBuildContext context : buildConfigs) {
+				EpocEnginePlugin.runWithBldInfData(bldInfFilePath, 
+						new DefaultViewConfiguration(context, bldInfFilePath, new AcceptedNodesViewFilter()), 
+						new BldInfDataRunnableAdapter() {
+							public Object run(IBldInfData data) {
+								for (IExtension extension : data.getExtensions()) {
+									if (extension.getName() == null) {
+										extensions.add(extension);
+									}
 								}
+								for (IExtension extension : data.getTestExtensions()) {
+									if (extension.getName() == null) {
+										extensions.add(extension);
+									}
+								}
+								return null;
 							}
-							for (IExtension extension : data.getTestExtensions()) {
-								if (extension.getName() == null) {
-									extensions.add(extension);
-								}
-							}
-							return null;
-						}
-				});
+					});
+	
+				monitor.worked(1);
+			}
+		} finally {		
+			monitor.done();
 
-			monitor.worked(1);
+			SymbianBuildContextDataCache.endProjectOperation();
 		}
-		
-		monitor.done();
 
 		return extensions.size() > 0;
 	}
--- a/builder/com.nokia.carbide.cdt.builder/src/com/nokia/carbide/cdt/internal/builder/CarbideBuildManager.java	Tue Jan 05 11:22:47 2010 -0600
+++ b/builder/com.nokia.carbide.cdt.builder/src/com/nokia/carbide/cdt/internal/builder/CarbideBuildManager.java	Tue Jan 05 11:23:50 2010 -0600
@@ -309,6 +309,7 @@
 		for (ICarbideBuildConfiguration config : cpi.getBuildConfigurations()) {
 
 			// force a rebuild of the CarbideLanguageData cache
+			// TODO PERFORMANCE EJS: why??? We end up forcing a cache rebuild even when you just switch configurations...
 			CLanguageData languageData = null;
 			ICProjectDescription projDes = CoreModel.getDefault().getProjectDescription(config.getCarbideProject().getProject());
 			if (projDes != null) {
--- a/builder/com.nokia.carbide.cdt.builder/src/com/nokia/carbide/cdt/internal/builder/CarbideProjectModifier.java	Tue Jan 05 11:22:47 2010 -0600
+++ b/builder/com.nokia.carbide.cdt.builder/src/com/nokia/carbide/cdt/internal/builder/CarbideProjectModifier.java	Tue Jan 05 11:23:50 2010 -0600
@@ -20,6 +20,7 @@
 import com.nokia.carbide.cdt.builder.project.ICarbideBuildConfiguration;
 import com.nokia.carbide.cdt.builder.project.ICarbideProjectModifier;
 import com.nokia.carbide.cdt.internal.api.builder.CarbideConfigurationDataProvider;
+import com.nokia.carbide.cpp.internal.api.sdk.SymbianBuildContextDataCache;
 import com.nokia.carbide.cpp.sdk.core.ISymbianBuildContext;
 import com.nokia.cpp.internal.api.utils.core.Logging;
 
@@ -255,8 +256,17 @@
 			CarbideBuilderPlugin.getBuildManager().setProjectInfo(this);
 			
 			// save the CDT project description
-			CCorePlugin.getDefault().setProjectDescription(projectTracker.getProject(), projDes, true, new NullProgressMonitor());
 			
+			try {
+				// let the build context caches know we may be iterating them all
+				SymbianBuildContextDataCache.startProjectOperation();
+				
+				// TODO PERFORMANCE: this can lead to CarbideLanguageData#buildCache(), which is an enormously expensive operation. 
+				// So use a real progress monitor, say from a Job, so UI will be updated
+				CCorePlugin.getDefault().setProjectDescription(projectTracker.getProject(), projDes, true, new NullProgressMonitor());
+			} finally {
+				SymbianBuildContextDataCache.endProjectOperation();
+			}
 			if (rebuildCacheAndReindex) {
 				ICProject cproject = CoreModel.getDefault().create(projectTracker.getProject());
 				if (cproject != null)
--- a/core/com.nokia.carbide.cpp.sdk.core/src/com/nokia/carbide/cpp/internal/api/sdk/SymbianBuildContext.java	Tue Jan 05 11:22:47 2010 -0600
+++ b/core/com.nokia.carbide.cpp.sdk.core/src/com/nokia/carbide/cpp/internal/api/sdk/SymbianBuildContext.java	Tue Jan 05 11:23:50 2010 -0600
@@ -18,13 +18,9 @@
 import org.eclipse.core.runtime.IPath;
 import org.osgi.framework.Version;
 
-import com.nokia.carbide.cpp.epoc.engine.model.sbv.ISBVView;
 import com.nokia.carbide.cpp.epoc.engine.preprocessor.*;
 import com.nokia.carbide.cpp.internal.sdk.core.model.SymbianMissingSDKFactory;
-import com.nokia.carbide.cpp.internal.sdk.core.model.SymbianSDK;
 import com.nokia.carbide.cpp.sdk.core.*;
-import com.nokia.carbide.internal.api.cpp.epoc.engine.preprocessor.BasicIncludeFileLocator;
-import com.nokia.carbide.internal.api.cpp.epoc.engine.preprocessor.MacroScanner;
 
 public class SymbianBuildContext implements ISymbianBuildContext {
 
@@ -43,22 +39,11 @@
 	// a copy of bad SDK default to fall back
 	private static ISymbianSDK fallbackForBadSdk = SymbianMissingSDKFactory.createInstance("dummy_ID"); //$NON-NLS-1$
 	
-	// last time we checked the hrh file mod dates - only check if changed in last second
-	private static final long HRH_TIMESTAMP_CHECK_QUANTUM = 1000; // 1 sec
-	private static long lastHrhTimestampCheck;
-	
-	private File prefixFileParsed;
-	private List<File> hrhFilesParsed = new ArrayList<File>();
-	private List<IDefine> variantHRHMacros = new ArrayList<IDefine>();
-	private long hrhCacheTimestamp;
-	private List<IDefine> compilerPrefixMacros = new ArrayList<IDefine>();
-	private long compilerCacheTimestamp;
-	
-	
 	public SymbianBuildContext(ISymbianSDK theSDK, String thePlatform, String theTarget) {
 		sdkId = theSDK.getUniqueId();
 		platform = thePlatform;
 		target = theTarget;
+		
 		getDisplayString();
 	}
 
@@ -329,150 +314,11 @@
 
 	public List<IDefine> getVariantHRHDefines() {
 
-		// we parse the variant hrh file to gather macros.  this can be time consuming so do it
-		// once and cache the values.  only reset the cache when the hrh or any of its includes
-		// has changed.
-		
-		boolean buildCache = false;
-		
-		if (hrhCacheTimestamp == 0) {
-			// hasn't been built yet
-			buildCache = true;
-		} else {
-			// cache exists.  see if any of the files have changed
-			ISymbianSDK sdk = getSDK();
-			if (sdk != null) {
-				// the prefix may have been added, removed, or changed.  in any case,
-				// we would need to reset the cache
-				File currentPrefixFile = sdk.getPrefixFile();
-				if (currentPrefixFile == null) {
-					if (prefixFileParsed != null) {
-						// prefix file was removed from the SDK
-						buildCache = true;
-					}
-				} else {
-					if (prefixFileParsed == null) {
-						// prefix file was added to the SDK
-						buildCache = true;
-					} else {
-						// there was a prefix file before and now.  see if it's the same file
-						// and if so, has it been modified?
-						if (!currentPrefixFile.equals(prefixFileParsed) || currentPrefixFile.lastModified() > hrhCacheTimestamp) {
-							buildCache = true;
-						}
-					}
-				}
-			}
-			
-			// now check to see if any of the included hrh files have changed
-			// we will do this at most once per quantum, because it is expensive and during import it was done 100 times per second
-			if (!buildCache && (System.currentTimeMillis() - lastHrhTimestampCheck) > HRH_TIMESTAMP_CHECK_QUANTUM) {
-				for (File file : hrhFilesParsed) {
-					if (file.lastModified() > hrhCacheTimestamp) {
-						buildCache = true;
-						break;
-					}
-				}
-				lastHrhTimestampCheck = System.currentTimeMillis();
-			}
-		}
-		
-		if (buildCache) {
-
-			variantHRHMacros.clear();
-			
-			synchronized (this) {
-
-				List<IDefine> macros = new ArrayList<IDefine>();
-				Map<String, IDefine> namedMacros = new HashMap<String, IDefine>();
-				File prefixFile = getSDK().getPrefixFile();
-				
-				if (prefixFile == null){
-					// Check that the prefix file may have become available since the SDK was scanned last.
-					// This can happen, for e.g., if the user opens the IDE _then_ does a subst on a drive that already has an SDK entry.
-					IPath prefixCheck = ((SymbianSDK)getSDK()).getPrefixFromVariantCfg();
-					if (prefixCheck != null){
-						prefixFile = prefixCheck.toFile();
-						getSDK().setPrefixFile(prefixCheck);
-					}
-				}
-				
-				if (prefixFile != null) {
-
-					// add any BSF/SBV includes so the headers are picked up from the correct location
-					List<File> systemPaths = new ArrayList<File>();
-					IBSFPlatform bsfPlat = getSDK().getBSFCatalog().findPlatform(platform);
-					ISBVPlatform sbvPlat = getSDK().getSBVCatalog().findPlatform(platform);
-					if (bsfPlat != null) {
-						for (IPath path : bsfPlat.getSystemIncludePaths()) {
-							systemPaths.add(path.toFile());
-						}
-					} else if (sbvPlat != null) {
-						LinkedHashMap<IPath, String> platPaths = sbvPlat.getBuildIncludePaths();
-						Set<IPath> set = platPaths.keySet();
-						for (IPath path : set) {
-							String pathType = platPaths.get(path);
-							if (pathType.equalsIgnoreCase(ISBVView.INCLUDE_FLAG_PREPEND) || pathType.equalsIgnoreCase(ISBVView.INCLUDE_FLAG_SET)){
-								systemPaths.add(path.toFile());
-							}
-						}
-					}
-					
-					MacroScanner scanner = new MacroScanner(
-							new BasicIncludeFileLocator(null, systemPaths.toArray(new File[systemPaths.size()])),
-							DefaultModelDocumentProvider.getInstance(),
-							DefaultTranslationUnitProvider.getInstance());
-					scanner.scanFile(prefixFile);
-		
-					List<IDefine> scannedMacros = (List<IDefine>)scanner.getMacroDefinitions();
-					for (IDefine scannedMacro : scannedMacros){
-						// we don't want duplicate macros, so check to see if it's already there.
-						// if it is, remove it and then add the newer one.  this is consistent with
-						// how it would be from a compiler standpoint.
-						IDefine macro = namedMacros.get(scannedMacro.getName());
-						if (macro != null) {
-							macros.remove(macro);
-						}
-						
-						macros.add(scannedMacro);
-						namedMacros.put(scannedMacro.getName(), scannedMacro);
-					}
-					
-					hrhFilesParsed.clear();
-					for (File inc : scanner.getIncludedFiles()) {
-						hrhFilesParsed.add(inc);
-					}
-					
-					List<String> variantCFGMacros = new ArrayList<String>();
-					variantCFGMacros = getSDK().getVariantCFGMacros();
-					for (String cfgMacros : variantCFGMacros){
-						// we don't want duplicate macros, so check to see if it's already there.
-						IDefine existingMacro = namedMacros.get(cfgMacros);
-						if (existingMacro != null) {
-							macros.remove(existingMacro);
-						}
-						
-						IDefine macro = DefineFactory.createSimpleFreeformDefine(cfgMacros);
-						macros.add(macro);
-						namedMacros.put(macro.getName(), macro);
-					}
-
-					// store off the time when we created the cache
-					hrhCacheTimestamp = System.currentTimeMillis();
-				}
-				
-				// save the prefix file (which may be null)
-				prefixFileParsed = prefixFile;
-
-				variantHRHMacros = macros;
-			}
-		}
-			
-		return variantHRHMacros;
+		return getCachedData().getVariantHRHDefines();
 	}
 
 	public List<File> getPrefixFileIncludes() {
-		return hrhFilesParsed;
+		return getCachedData().getPrefixFileIncludes();
 	}
 
 
@@ -481,54 +327,11 @@
 		// once and cache the values.  only reset the cache when the compiler prefix has changed.
 		
 		IPath prefixFile = getCompilerPrefixFile();
-		if (prefixFile == null || !prefixFile.toFile().exists()) {
-			compilerPrefixMacros.clear();
-			return compilerPrefixMacros;
+		if (prefixFile == null) {
+			return Collections.emptyList();
 		}
-
-		if ((compilerCacheTimestamp == 0) ||
-				(prefixFile.toFile().lastModified() > compilerCacheTimestamp)) {
-
-			compilerPrefixMacros.clear();
-			
-			synchronized (this) {
-
-				List<IDefine> macros = new ArrayList<IDefine>();
-				if (prefixFile != null) {
-
-					List<File> userPaths = new ArrayList<File>();
-					List<File> systemPaths = new ArrayList<File>();
-					
-					userPaths.add(prefixFile.removeLastSegments(1).toFile());
-					systemPaths.add(prefixFile.removeLastSegments(1).toFile());
-					IPath includePath = getSDK().getIncludePath();
-					if (includePath != null) {
-						File includeDir = includePath.toFile().getAbsoluteFile();
-						userPaths.add(includeDir);
-						systemPaths.add(includeDir);
-					}
-
-					
-					// get macros from the compiler prefix file: note, this is a stupid
-					// scan that will get the last version #defined, even if inside an #if.
-					MacroScanner scanner = new MacroScanner(
-							new BasicIncludeFileLocator(userPaths.toArray(new File[userPaths.size()]), systemPaths.toArray(new File[systemPaths.size()])),
-							DefaultModelDocumentProvider.getInstance(), 
-							DefaultTranslationUnitProvider.getInstance());
-					scanner.scanFile(prefixFile.toFile());
-					for (IDefine define : scanner.getMacroDefinitions()) {
-						macros.add(define);
-					}
-
-					// store off the time when we created the cache
-					compilerCacheTimestamp = System.currentTimeMillis();
-				}
-				
-				compilerPrefixMacros = macros;
-			}
-		}
-			
-		return compilerPrefixMacros;
+		
+		return getCachedData().getCompilerMacros(prefixFile);
 	}
 
 
@@ -552,6 +355,16 @@
 		}
 	}
 
+	/**
+	 * Get the cache holding the data that applies to this build context globally.
+	 * A build context is subclassed by CarbideBuildConfiguration, which has multiple
+	 * instances at runtime, thus, a SymbianBuildContext instance should not hold a cache itself.
+	 * @return cache, never <code>null</code>
+	 */
+	private SymbianBuildContextDataCache getCachedData() {
+		return SymbianBuildContextDataCache.getCache(this);
+	}
+	
 
 	public String getBasePlatformForVariation() {
 		String plat = "";
@@ -565,6 +378,14 @@
 		
 		return plat;
 	}
-	
-	
+
+
+	/**
+	 * Get the list of #include paths detected for this context.
+	 * @return List or <code>null</code>
+	 */
+	public List<File> getCachedSystemIncludePaths() {
+		return getCachedData().getSystemIncludePaths();
+	}
+
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/com.nokia.carbide.cpp.sdk.core/src/com/nokia/carbide/cpp/internal/api/sdk/SymbianBuildContextDataCache.java	Tue Jan 05 11:23:50 2010 -0600
@@ -0,0 +1,702 @@
+/*
+* Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies). 
+* All rights reserved.
+* This component and the accompanying materials are made available
+* under the terms of the License "Eclipse Public License v1.0"
+* which accompanies this distribution, and is available
+* at the URL "http://www.eclipse.org/legal/epl-v10.html".
+*
+* Initial Contributors:
+* Nokia Corporation - initial contribution.
+*
+*/
+package com.nokia.carbide.cpp.internal.api.sdk;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.ObjectStreamClass;
+import java.io.ObjectStreamException;
+import java.util.*;
+
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.core.runtime.Platform;
+
+import com.nokia.carbide.cpp.epoc.engine.EpocEnginePlugin;
+import com.nokia.carbide.cpp.epoc.engine.model.sbv.ISBVView;
+import com.nokia.carbide.cpp.epoc.engine.preprocessor.*;
+import com.nokia.carbide.cpp.internal.sdk.core.model.SymbianSDK;
+import com.nokia.carbide.cpp.sdk.core.*;
+import com.nokia.carbide.internal.api.cpp.epoc.engine.preprocessor.BasicIncludeFileLocator;
+import com.nokia.carbide.internal.api.cpp.epoc.engine.preprocessor.MacroScanner;
+import com.nokia.cpp.internal.api.utils.core.ExternalFileInfoCollection;
+import com.nokia.cpp.internal.api.utils.core.FileUtils;
+import com.nokia.cpp.internal.api.utils.core.Logging;
+import com.nokia.cpp.internal.api.utils.core.ObjectUtils;
+
+/**
+ * This class holds the externally gathered data for a build context,
+ * such as the #include paths and macros defined in the SDK.
+ * <p> 
+ * Unlike CarbideBuildConfiguration (which, unfortunately extends 
+ * SymbianBuildContext), this will not be created multiple times for
+ * multiple projects, but only once for a different build context
+ * in an SDK or devkit.
+ */
+public class SymbianBuildContextDataCache {
+
+	public static boolean DEBUG = false;
+	
+	// by default, only check HRH-included files if changed in last second (for ordinary operations)
+	// or the last minute (when doing a long project operation).  see #startThrottle() and #stopThrottle()
+	private static final long DEFAULT_HRH_INFO_CHECK_QUANTUM = 1000; // 1 sec
+	private static final long THROTTLED_HRH_INFO_CHECK_QUANTUM = 60000; // 60 sec
+
+	// compiler prefixes are very unlikely to change, but we need to check
+	// occasionally in case a user installs a new one...
+	private static final long DEFAULT_COMPILER_PREFIX_INFO_CHECK_QUANTUM = 15 * 60 * 1000;	// 15 minutes
+	
+	// This is a count of times #startProjectOperation() was called without
+	// balancing #endProjectOperation().  
+	private static int inProjectOperationCount;
+	
+	private static Map<String, SymbianBuildContextDataCache> cacheMap = new HashMap<String, SymbianBuildContextDataCache>();
+	
+	public static synchronized SymbianBuildContextDataCache getCache(ISymbianBuildContext context) {
+		// don't hash on ISymbianBuildContext itself since it is sometimes a ICarbideBuildConfiguration
+		String key = getBuildContextKey(context);
+		
+		SymbianBuildContextDataCache cache = cacheMap.get(key);
+		if (cache == null) {
+			cache = new SymbianBuildContextDataCache(context);
+			cache.loadCacheFile();
+			cacheMap.put(key, cache);
+		}
+		return cache;
+	}
+
+	/**
+	 * @param context
+	 * @return
+	 */
+	private static String getBuildContextKey(ISymbianBuildContext context) {
+		String key = context.getPlatformString() + "/" + context.getTargetString() + "/";
+		ISymbianSDK sdk = context.getSDK();
+		if (sdk != null)
+			key += sdk.getEPOCROOT();
+		return key;
+	}
+
+	//private File prefixFileParsed;
+	private List<File> hrhFilesParsed = new ArrayList<File>();
+	private ExternalFileInfoCollection hrhFileInfo = null; 
+	private List<IDefine> variantHRHMacros = new ArrayList<IDefine>();
+	private List<IDefine> compilerPrefixMacros = new ArrayList<IDefine>();
+	private ExternalFileInfoCollection compilerPrefixFileInfo = null; 
+	private List<File> systemIncludes;
+	private ISymbianSDK sdk;
+	private IPath compilerPrefixFile;
+
+	private String platformString;
+
+	private String displayString;
+
+	private String contextKey;
+	
+	private boolean changed;
+
+	private File cacheFile;
+
+	private SymbianBuildContextDataCache(ISymbianBuildContext context) {
+		if (DEBUG) System.out.println("Creating cache for " + context.getDisplayString());
+		this.platformString = context.getPlatformString();
+		this.displayString = context.getDisplayString();
+		this.sdk = context.getSDK();
+		this.contextKey = getBuildContextKey(context);
+	}
+
+	/* (non-Javadoc)
+	 * @see java.lang.Object#toString()
+	 */
+	@Override
+	public String toString() {
+		return "Cache for " + displayString;
+	}
+	
+	public List<IDefine> getVariantHRHDefines() {
+
+		// we parse the variant hrh file to gather macros.  this can be time consuming so do it
+		// once and cache the values.  only reset the cache when the hrh or any of its includes
+		// has changed.
+		
+		boolean buildCache = false;
+		
+		if (hrhFileInfo == null) {
+			// hasn't been built yet, or was flushed
+			buildCache = true;
+		} else {
+			// Cache exists.  See if any of the files have changed
+			if (sdk != null && hrhFileInfo.anyChanged()) {
+				buildCache = true;
+			}
+		}
+		
+		if (buildCache) {
+			gatherVariantHRHDefines();
+		}
+			
+		return variantHRHMacros;
+	}
+
+	/**
+	 * Re-gather the #defines from the variant HRH file 
+	 */
+	private void gatherVariantHRHDefines() {
+		changed = true;
+		variantHRHMacros.clear();
+		
+		synchronized (this) {
+
+			List<IDefine> macros = new ArrayList<IDefine>();
+			Map<String, IDefine> namedMacros = new HashMap<String, IDefine>();
+			File prefixFile = sdk.getPrefixFile();
+			
+			if (prefixFile == null){
+				// Check that the prefix file may have become available since the SDK was scanned last.
+				// This can happen, for e.g., if the user opens the IDE _then_ does a subst on a drive that already has an SDK entry.
+				IPath prefixCheck = ((SymbianSDK)sdk).getPrefixFromVariantCfg();
+				if (prefixCheck != null){
+					prefixFile = prefixCheck.toFile();
+					sdk.setPrefixFile(prefixCheck);
+				}
+			}
+			
+			File[] includedFiles = null;
+
+			if (prefixFile != null) {
+
+				// add any BSF/SBV includes so the headers are picked up from the correct location
+				List<File> systemPaths = new ArrayList<File>();
+				IBSFPlatform bsfPlat = sdk.getBSFCatalog().findPlatform(platformString);
+				ISBVPlatform sbvPlat = sdk.getSBVCatalog().findPlatform(platformString);
+				if (bsfPlat != null) {
+					for (IPath path : bsfPlat.getSystemIncludePaths()) {
+						systemPaths.add(path.toFile());
+					}
+				} else if (sbvPlat != null) {
+					LinkedHashMap<IPath, String> platPaths = sbvPlat.getBuildIncludePaths();
+					Set<IPath> set = platPaths.keySet();
+					for (IPath path : set) {
+						String pathType = platPaths.get(path);
+						if (pathType.equalsIgnoreCase(ISBVView.INCLUDE_FLAG_PREPEND) || pathType.equalsIgnoreCase(ISBVView.INCLUDE_FLAG_SET)){
+							systemPaths.add(path.toFile());
+						}
+					}
+				}
+				
+				MacroScanner scanner = new MacroScanner(
+						new BasicIncludeFileLocator(null, systemPaths.toArray(new File[systemPaths.size()])),
+						DefaultModelDocumentProvider.getInstance(),
+						DefaultTranslationUnitProvider.getInstance());
+				scanner.scanFile(prefixFile);
+
+				List<IDefine> scannedMacros = (List<IDefine>)scanner.getMacroDefinitions();
+				for (IDefine scannedMacro : scannedMacros){
+					// we don't want duplicate macros, so check to see if it's already there.
+					// if it is, remove it and then add the newer one.  this is consistent with
+					// how it would be from a compiler standpoint.
+					IDefine macro = namedMacros.get(scannedMacro.getName());
+					if (macro != null) {
+						macros.remove(macro);
+					}
+					
+					macros.add(scannedMacro);
+					namedMacros.put(scannedMacro.getName(), scannedMacro);
+				}
+				
+				hrhFilesParsed.clear();
+				includedFiles = scanner.getIncludedFiles();
+				for (File inc : includedFiles) {
+					hrhFilesParsed.add(inc);
+				}
+				
+				List<String> variantCFGMacros = new ArrayList<String>();
+				variantCFGMacros = sdk.getVariantCFGMacros();
+				for (String cfgMacros : variantCFGMacros){
+					// we don't want duplicate macros, so check to see if it's already there.
+					IDefine existingMacro = namedMacros.get(cfgMacros);
+					if (existingMacro != null) {
+						macros.remove(existingMacro);
+					}
+					
+					IDefine macro = DefineFactory.createSimpleFreeformDefine(cfgMacros);
+					macros.add(macro);
+					namedMacros.put(macro.getName(), macro);
+				}
+			} 
+			
+			// cache the info about when we created the cache
+			if (hrhFileInfo == null) {
+				hrhFileInfo = new ExternalFileInfoCollection(
+						EpocEnginePlugin.getExternalFileInfoCache(),
+						includedFiles,
+						DEFAULT_HRH_INFO_CHECK_QUANTUM);
+			} else {
+				hrhFileInfo.setFiles(includedFiles);
+			}
+
+			variantHRHMacros = macros;
+			saveCacheFile();
+		}
+	}
+
+	public List<File> getPrefixFileIncludes() {
+		return hrhFilesParsed;
+	}
+
+
+	public synchronized List<IDefine> getCompilerMacros(IPath prefixFile) {
+		
+		// we assume that the prefix file will not change often,
+		// (if at all) for a build context, so dump the cache if the prefix file changes.
+		
+		if (compilerPrefixFile != null && !compilerPrefixFile.equals(prefixFile)) {
+			compilerPrefixFileInfo = null;
+		}
+		
+		compilerPrefixFile = prefixFile;
+
+		if (compilerPrefixFileInfo == null ||
+				compilerPrefixFileInfo.anyChanged()) {
+
+			changed = true;
+
+			compilerPrefixMacros.clear();
+			
+			synchronized (this) {
+
+				List<IDefine> macros = new ArrayList<IDefine>();
+				if (prefixFile != null) {
+
+					List<File> userPaths = new ArrayList<File>();
+					List<File> systemPaths = new ArrayList<File>();
+					
+					userPaths.add(prefixFile.removeLastSegments(1).toFile());
+					systemPaths.add(prefixFile.removeLastSegments(1).toFile());
+					IPath includePath = sdk.getIncludePath();
+					if (includePath != null) {
+						File includeDir = includePath.toFile().getAbsoluteFile();
+						userPaths.add(includeDir);
+						systemPaths.add(includeDir);
+					}
+
+					
+					// get macros from the compiler prefix file: note, this is a stupid
+					// scan that will get the last version #defined, even if inside an #if.
+					MacroScanner scanner = new MacroScanner(
+							new BasicIncludeFileLocator(userPaths.toArray(new File[userPaths.size()]), systemPaths.toArray(new File[systemPaths.size()])),
+							DefaultModelDocumentProvider.getInstance(), 
+							DefaultTranslationUnitProvider.getInstance());
+					scanner.scanFile(prefixFile.toFile());
+					for (IDefine define : scanner.getMacroDefinitions()) {
+						macros.add(define);
+					}
+
+					// store off the info about what we read for this cache
+					File[] files = scanner.getIncludedFiles();
+					
+					if (compilerPrefixFileInfo == null)
+						compilerPrefixFileInfo = new ExternalFileInfoCollection(
+								EpocEnginePlugin.getExternalFileInfoCache(),
+								files,
+								DEFAULT_COMPILER_PREFIX_INFO_CHECK_QUANTUM);
+					else
+						compilerPrefixFileInfo.setFiles(files);
+				}
+				
+				compilerPrefixMacros = macros;
+				
+				saveCacheFile();
+			}
+		}
+			
+		return compilerPrefixMacros;
+	}
+
+	/**
+	 * Get the list of #include paths detected for this context.
+	 * @return List or <code>null</code>
+	 */
+	public synchronized List<File> getSystemIncludePaths() {
+		if (systemIncludes == null) {
+			gatherBuildContextSystemIncludePaths();
+		}
+		return systemIncludes;
+	}
+
+	/**
+	 * Fetch the list of include paths for the build context
+	 */
+	private void gatherBuildContextSystemIncludePaths() {
+		changed = true;
+		
+		systemIncludes = new ArrayList<File>();
+		
+		if (DEBUG) System.out.println("Scanning include paths for " + displayString);
+		
+		IBSFPlatform bsfplatform = sdk.getBSFCatalog().findPlatform(platformString);
+		ISBVPlatform sbvPlatform = sdk.getSBVCatalog().findPlatform(platformString);
+
+		// look in the epoc32 directory of the SDK
+		IPath includePath = sdk.getIncludePath();
+		if (includePath != null) {
+			File includeDir = includePath.toFile().getAbsoluteFile();
+			File dir;
+			
+			// get additional include directories from BSF platform, if defined
+			if (bsfplatform != null) {
+				IPath[] systemIncludePaths = bsfplatform.getSystemIncludePaths();
+				for (IPath path : systemIncludePaths) {
+					dir = path.toFile();
+					if (dir.exists() && dir.isDirectory()) {
+						systemIncludes.add(dir);
+					}
+				}
+			} else if (sbvPlatform != null){
+				
+				LinkedHashMap<IPath, String> platPaths = sbvPlatform.getBuildIncludePaths();
+				Set<IPath> set = platPaths.keySet();
+				for (IPath path : set) {
+					String pathType = platPaths.get(path);
+					if (pathType.equalsIgnoreCase(ISBVView.INCLUDE_FLAG_PREPEND) || pathType.equalsIgnoreCase(ISBVView.INCLUDE_FLAG_SET)){
+						dir = path.toFile();
+						systemIncludes.add(dir);
+					}
+				}
+			}
+			else {
+				// legacy behavior 
+				if (platformString.equals(ISymbianBuildContext.EMULATOR_PLATFORM)) {
+					dir = new File(includeDir, "wins"); //$NON-NLS-1$
+					if (dir.exists() && dir.isDirectory()) {
+						systemIncludes.add(dir);
+					}
+				}
+			}
+
+			// add OEM dir
+			dir = new File(includeDir, "oem"); //$NON-NLS-1$
+			if (dir.exists() && dir.isDirectory()) {
+				systemIncludes.add(dir);
+			}
+
+			// and finally the normal include dir
+			systemIncludes.add(includeDir);
+			
+			// and finally, finally, if this is an SBV add any paths with the append flag
+			if (sbvPlatform != null){
+				
+				Map<IPath, String> platPaths = sbvPlatform.getBuildIncludePaths();
+				Set<IPath> set = platPaths.keySet();
+				for (IPath path : set) {
+					String pathType = platPaths.get(path);
+					if (pathType.equalsIgnoreCase(ISBVView.INCLUDE_FLAG_APPEND)){
+						dir = path.toFile();
+						systemIncludes.add(dir);
+					}
+				}	
+			}
+		}
+		
+		// also search files in same folder as variant.hrh
+		File prefix = sdk.getPrefixFile();
+		if (sbvPlatform != null){
+			// might be an alternate HRH file to use
+			IPath varVarHRH = sbvPlatform.getBuildVariantHRHFile();
+			if (!varVarHRH.toFile().equals(prefix) && varVarHRH.toFile().exists()){
+				prefix = varVarHRH.toFile();
+			} 
+		}
+		if (prefix != null) {
+			systemIncludes.add(prefix.getParentFile());
+		}
+		
+		saveCacheFile();
+	}
+
+	/**
+	 * Let cache know that the client is beginning an operation that
+	 * will iterate all the build contexts.  We use this information
+	 * to optimize file info checks.
+	 * <p>
+	 * Each call must be balanced by an {@link #endProjectOperation()} call.
+	 * Use a try ... finally block to make sure.
+	 */
+	public static synchronized void startProjectOperation() {
+		inProjectOperationCount++;
+		if (inProjectOperationCount == 1) {
+			for (SymbianBuildContextDataCache cache : cacheMap.values()) {
+				cache.startThrottle();
+			}
+		}
+	}
+
+	/**
+	 * Let cache know that a project-wide operation is done, so
+	 * we can resume normal info checking behavior.
+	 */
+	public static synchronized void endProjectOperation() {
+		inProjectOperationCount--;
+		if (inProjectOperationCount < 0) {
+			Logging.log(SDKCorePlugin.getDefault(), 
+					Logging.newStatus(SDKCorePlugin.getDefault(), 
+							new IllegalStateException("project operation count not balanced")));
+			inProjectOperationCount = 0;
+		}
+		if (inProjectOperationCount == 0) {
+			for (SymbianBuildContextDataCache cache : cacheMap.values()) {
+				cache.stopThrottle();
+				cache.saveCacheFile();
+			}
+		}
+	}
+
+	/**
+	 * Throttle file info checks on this cache if we suspect the same
+	 * files will be checked over and over again in a short time (e.g. through
+	 * multiple contexts on the same SDK).
+	 */
+	private void startThrottle() {
+		if (hrhFileInfo != null) {
+			hrhFileInfo.setRecheckQuantum(THROTTLED_HRH_INFO_CHECK_QUANTUM);
+		}
+		// note: compiler prefix infos already have a long delay, but
+		// this is a good place to refresh
+		if (compilerPrefixFileInfo != null) {
+			compilerPrefixFileInfo.refresh();
+		}
+	}
+
+	/**
+	 * End file info throttling.
+	 */
+	private void stopThrottle() {
+		if (hrhFileInfo != null)
+			hrhFileInfo.setRecheckQuantum(DEFAULT_HRH_INFO_CHECK_QUANTUM);
+		// note: compiler prefix infos already have a long delay
+	}
+
+	/**
+	 * Refresh the cached data when there are substantial changes in the given SDKs.
+	 * 
+	 * @param sdks SDKs for whose contexts the caches should be removed, or <code>null</code> for all of them
+	 */
+	public static synchronized void refreshForSDKs(ISymbianSDK[] sdks) {
+		// refresh each context cache, meaning, delete its memory and disk values
+		Collection<String> values = cacheMap.keySet();
+		String[] keyArray = (String[]) values.toArray(new String[values.size()]);
+		for (String key : keyArray) {
+			SymbianBuildContextDataCache cache = cacheMap.get(key);
+			boolean forSDK = sdkInArray(sdks, cache.sdk);
+			if (forSDK) {
+				cache.reset();
+				cacheMap.remove(key);
+			}
+		}
+	}
+
+	private static boolean sdkInArray(ISymbianSDK[] sdks, ISymbianSDK anSDK) {
+		if (sdks == null)
+			return true;
+		if (anSDK == null)
+			return false;
+		// object identity is not appropriate; user may have renamed or moved an SDK
+		for (ISymbianSDK sdk : sdks) {
+			if (ObjectUtils.equals(sdk.getEPOCROOT(), anSDK.getEPOCROOT())
+					|| ObjectUtils.equals(sdk.getUniqueId(), anSDK.getUniqueId()))
+				return true;
+		}
+		return false;
+	}
+	
+	/**
+	 * Reset the cached data for this context, ensuring it will be
+	 * freshly gathered.  This deletes anything stored on disk.
+	 */
+	public synchronized void reset() {
+		hrhFileInfo = null;
+		compilerPrefixFileInfo = null;
+		systemIncludes = null;
+		getCacheFile().delete();
+	}
+
+	/**
+	 * Get the file where we store cached data
+	 * @return File in workspace plugin state storage
+	 */
+	protected File getCacheFile() {
+		if (cacheFile == null) {
+			IPath statePath;
+			if (Platform.isRunning())
+				statePath = Platform.getStateLocation(SDKCorePlugin.getDefault().getBundle());
+			else
+				statePath = new Path(FileUtils.getTemporaryDirectory().getAbsolutePath());
+			cacheFile = statePath.append(contextKey.replaceAll("[^A-Za-z0-9_]", "_") + ".dat").toFile(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+		}
+		return cacheFile;
+	}
+	
+	/**
+	 * Save cache to disk if changed.
+	 */
+	protected void saveCacheFile() {
+		if (!changed)
+			return;
+			
+		ObjectOutputStream os = null;
+		try {
+			File cacheFile = getCacheFile();
+			if (DEBUG) System.out.println("Saving to " + cacheFile); //$NON-NLS-1$
+			os = new ObjectOutputStream(new FileOutputStream(cacheFile));
+			doSaveCache(os);
+			changed = false;
+		} catch (ObjectStreamException e) {
+			Logging.log(SDKCorePlugin.getDefault(), 
+					Logging.newStatus(SDKCorePlugin.getDefault(), 
+							IStatus.WARNING,
+							"Tried to save uncacheable data for " + displayString, //$NON-NLS-1$
+							e));
+		} catch (IOException e) {
+			// oh well
+			Logging.log(SDKCorePlugin.getDefault(), 
+					Logging.newStatus(SDKCorePlugin.getDefault(), 
+							IStatus.WARNING,
+							"Failed to save cache state for " + displayString, //$NON-NLS-1$
+							e));
+		} finally {
+			if (os != null) {
+				try {
+					os.close();
+				} catch (IOException e) {
+				}
+			}
+		}
+	}
+	
+	protected void loadCacheFile() {
+		ObjectInputStream is = null;
+		// Use the class loader that knows how to span plugins
+		final ClassLoader classLoader = SDKCorePlugin.getDefault().getClass().getClassLoader();
+		File cacheFile = getCacheFile();
+		try {
+			is = new ObjectInputStream(new FileInputStream(cacheFile)) {
+				/* (non-Javadoc)
+				 * @see java.io.ObjectInputStream#resolveClass(java.io.ObjectStreamClass)
+				 */
+				@Override
+				protected Class<?> resolveClass(ObjectStreamClass desc)
+						throws IOException, ClassNotFoundException {
+					String name = desc.getName();
+					try {
+						return classLoader.loadClass(name);
+					} catch (ClassNotFoundException e) {
+						return super.resolveClass(desc);
+					}
+				}
+			};
+			doLoadCache(is);
+			if (DEBUG) System.out.println("Loaded cache from " + cacheFile); //$NON-NLS-1$
+		} catch (ClassNotFoundException e) {
+			// this is probably a bug
+			e.printStackTrace();
+		} catch (ObjectStreamException e) {
+			// assume it's just out of sync (e.g. object signatures changed), so nothing in Error Log
+			e.printStackTrace();
+			cacheFile.delete();
+		} catch (IOException e) {
+			// oh well, not made yet
+		} finally {
+			if (is != null) {
+				try {
+					is.close();
+				} catch (IOException e) {
+				}
+			}
+			changed = false;
+		}
+	}
+
+	/**
+	 * @param os
+	 */
+	private void doSaveCache(ObjectOutputStream os) throws IOException {
+		os.writeObject(systemIncludes);
+		savePath(os, compilerPrefixFile);
+		os.writeObject(compilerPrefixFileInfo);
+		os.writeObject(compilerPrefixMacros);
+		os.writeObject(hrhFileInfo);
+		os.writeObject(hrhFilesParsed);
+		os.writeObject(variantHRHMacros);
+	}
+
+	
+	/**
+	 * Load entries from the cache.  This must match the ordering in
+	 * {@link #doSaveCache(ObjectOutputStream)}.
+	 * @param is
+	 */
+	private void doLoadCache(ObjectInputStream is) throws IOException, ClassCastException, ClassNotFoundException {
+		systemIncludes = loadList(is, File.class);
+		compilerPrefixFile = loadPath(is);
+		compilerPrefixFileInfo = (ExternalFileInfoCollection) is.readObject();
+		compilerPrefixMacros = loadList(is, IDefine.class);
+		hrhFileInfo = (ExternalFileInfoCollection) is.readObject();
+		hrhFilesParsed = loadList(is, File.class);
+		variantHRHMacros = loadList(is, IDefine.class);
+	}
+
+	/**
+	 * Read an IPath from a portable string
+	 * @param is
+	 * @return an IPath constructed from a String
+	 */
+	private IPath loadPath(ObjectInputStream is) throws IOException, ClassCastException, ClassNotFoundException {	
+		String path;
+		path = (String) is.readObject();
+		if (path != null)
+			return Path.fromPortableString(path);
+		return null;
+	}
+	
+	/**
+	 * Save an IPath as a portable string
+	 * @param os
+	 * @param path
+	 * @throws IOException
+	 */
+	private void savePath(ObjectOutputStream os, IPath path) throws IOException {
+		os.writeObject(path != null ? path.toPortableString() : null);
+	}
+
+	/**
+	 * Load a list from the object stream, throwing if it appears to contain
+	 * the wrong kind of data.
+	 * @param is
+	 * @param klass
+	 * @return a List or <code>null</code>
+	 */
+	@SuppressWarnings("unchecked")
+	private <T> List<T> loadList(ObjectInputStream is, Class<T> klass) throws IOException, ClassCastException, ClassNotFoundException { 
+		List<T> list = (List) is.readObject();
+		if (list == null || klass == null || list.size() == 0)
+			return list;
+		if (!klass.isInstance(list.get(0)))
+			throw new IOException("Class contains " + list.get(0).getClass() + ", not " + klass); //$NON-NLS-1$ //$NON-NLS-2$
+		return list;
+	}
+}
--- a/core/com.nokia.carbide.cpp.sdk.core/src/com/nokia/carbide/cpp/internal/sdk/core/model/AbstractSDKManager.java	Tue Jan 05 11:22:47 2010 -0600
+++ b/core/com.nokia.carbide.cpp.sdk.core/src/com/nokia/carbide/cpp/internal/sdk/core/model/AbstractSDKManager.java	Tue Jan 05 11:23:50 2010 -0600
@@ -52,6 +52,7 @@
 import com.nokia.carbide.cpp.internal.api.sdk.ISDKManagerInternal;
 import com.nokia.carbide.cpp.internal.api.sdk.SBSv2Utils;
 import com.nokia.carbide.cpp.internal.api.sdk.SDKManagerInternalAPI;
+import com.nokia.carbide.cpp.internal.api.sdk.SymbianBuildContextDataCache;
 import com.nokia.carbide.cpp.internal.api.sdk.SymbianMacroStore;
 import com.nokia.carbide.cpp.sdk.core.ICarbideInstalledSDKChangeListener;
 import com.nokia.carbide.cpp.sdk.core.IRVCTToolChainInfo;
@@ -122,7 +123,7 @@
 	public void scanSDKs() {
 		synchronized (sdkList)
 		{
-			ArrayList<ISymbianSDK> oldSDkList = new ArrayList<ISymbianSDK>(sdkList);
+			ArrayList<ISymbianSDK> oldSDKList = new ArrayList<ISymbianSDK>(sdkList);
 			
 			getSBSv2Version(true);
 			
@@ -143,7 +144,7 @@
 
 			// now these SDK's are removed from the old list, add to
 			// internal list
-			for (ISymbianSDK oldSdk : oldSDkList) {
+			for (ISymbianSDK oldSdk : oldSDKList) {
 				boolean found = false;
 				for (ISymbianSDK sdk : sdkList) {
 					if (sdk.getUniqueId().equals(oldSdk.getUniqueId())) {
@@ -154,6 +155,8 @@
 				if (found == false) {
 					SDKManagerInternalAPI.addMissingSdk(oldSdk
 							.getUniqueId());
+					// flush cache
+					SymbianBuildContextDataCache.refreshForSDKs(new ISymbianSDK[] { oldSdk });
 				}
 			}
 
@@ -172,7 +175,7 @@
 	 * Actually scan the SDKs available and populate sdkList.   
 	 * @param oldSDkList old list of SDKs available for reference, e.g., detecting
 	 * when SDKs are newly missing
-	 * @return
+	 * @return true if scan succeeded
 	 */
 	abstract protected boolean doScanSDKs();
 	
--- a/core/com.nokia.carbide.cpp.sdk.ui/src/com/nokia/carbide/cpp/internal/sdk/ui/SDKPreferencePage.java	Tue Jan 05 11:22:47 2010 -0600
+++ b/core/com.nokia.carbide.cpp.sdk.ui/src/com/nokia/carbide/cpp/internal/sdk/ui/SDKPreferencePage.java	Tue Jan 05 11:23:50 2010 -0600
@@ -33,6 +33,7 @@
 import org.eclipse.swt.widgets.*;
 import org.eclipse.ui.*;
 
+import com.nokia.carbide.cpp.internal.api.sdk.SymbianBuildContextDataCache;
 import com.nokia.carbide.cpp.internal.sdk.core.model.SDKManager;
 import com.nokia.carbide.cpp.sdk.core.*;
 import com.nokia.carbide.cpp.sdk.ui.SDKUIPlugin;
@@ -370,6 +371,8 @@
 			if (sdkPropDlg.open() == SDKPropertiesDialog.OK){
 				sdkListTableViewer.refresh();
 				setSelectedSDKInfoText(sdk);
+				// forcible rescan; dump cache
+				SymbianBuildContextDataCache.refreshForSDKs(new ISymbianSDK[] { sdk });
 				rescanSDKs(false);
 			}
 		} else {
@@ -405,6 +408,8 @@
 	}
 	
 	private void rescanNowButtonAction(){
+		// forcible rescan; dump cache
+		SymbianBuildContextDataCache.refreshForSDKs(null);
 		rescanSDKs(true);
 	}
 	
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/com.nokia.cpp.utils.core/src/com/nokia/cpp/internal/api/utils/core/ExternalFileInfoCache.java	Tue Jan 05 11:23:50 2010 -0600
@@ -0,0 +1,183 @@
+/*
+* Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies).
+* All rights reserved.
+* This component and the accompanying materials are made available
+* under the terms of the License "Eclipse Public License v1.0"
+* which accompanies this distribution, and is available
+* at the URL "http://www.eclipse.org/legal/epl-v10.html".
+*
+* Initial Contributors:
+* Nokia Corporation - initial contribution.
+*
+* Contributors:
+*
+* Description: 
+*
+*/
+
+package com.nokia.cpp.internal.api.utils.core;
+
+import java.io.File;
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Use this object to cache the timestamps and sizes for files which
+ * are often queried. On some OSes or types of filesystems, attribute checking
+ * is very slow, and on others, the resolution of a timestamp is so low that
+ * checking too often is likely to be a waste of effort.
+ * <p>
+ * We only recommend using this tracker for files outside the Eclipse
+ * workspace.  For workspace resources, use resource listeners to avoid 
+ * polling for file changes.   
+ */
+public class ExternalFileInfoCache implements Serializable {
+	
+	private static final long serialVersionUID = -2810901674805567105L;
+	
+	public static boolean DEBUG = false;
+	public static boolean DEBUG_VERBOSE = false;
+	
+	// time in ms between checks of any given file
+	private int quantum;
+	
+	static class FileInfo extends Tuple implements Serializable {
+		
+		private static final long serialVersionUID = -1226913205921924292L;
+		
+		protected FileInfo() {
+			// empty for serialization
+		}
+		public FileInfo(long lastModified, long lastQueried, long size) {
+			super(lastModified, lastQueried, size);
+		}
+		public long getLastModified() {
+			return (Long) get(0);
+		}
+		public long getLastQueried() {
+			return (Long) get(1);
+		}
+		public long getLength() { 
+			return (Long) get(2);
+		}
+		
+		public boolean isChangedFrom(FileInfo other) {
+			long origTime = getLastModified();
+			long origSize = getLength();
+			
+			long newTime = other.getLastModified();
+			long newSize = other.getLength();
+			
+			if (newTime == 0) { // 0 if deleted
+				return true;
+			}
+			if (origTime != newTime
+				|| origSize != newSize)
+				return true;
+			
+			return false;
+		}
+	}
+	
+	/** map of file to file size + last queried timestamp + time of last query 
+	 * (use File, not IPath, so we canonicalize for the OS) */
+	private Map<File, FileInfo> info = new HashMap<File, FileInfo>();		
+	
+	
+	/**
+	 * Create a cache to track the states of file timestamps and sizes.
+	 * @param quantumMs time in ms between updating the status of any given file
+	 */
+	public ExternalFileInfoCache(int quantumMs) {
+		this.quantum = quantumMs;
+	}
+
+	/**
+	 * Create a cache to track the states of file timestamps and sizes,
+	 * using an OS-dependent default quantum.
+	 */
+	public ExternalFileInfoCache() {
+		// this is based on the expensiveness of the attribute lookup, not the 
+		// resolution of the timestamps
+		this.quantum = HostOS.IS_WIN32 ? 50 : 10;
+	}
+
+	/** 
+	 * Tell if the file's timestamp or size changed since the basis time,
+	 * or otherwise changed in the past quantum,  
+	 * and update the record.
+	 * <p>
+	 * If a file does not exist, we always consider it to have changed
+	 * (since otherwise, we cannot tell if a client saw the transition
+	 * from existing to not existing).  
+	 * @param basis the system time in ms from which to judge the change
+	 * @return true if file size or timestamp changed since basis, or
+	 * the file does not exist
+	 */
+	public FileInfo getFileInfo(File file, long basis) {
+		FileInfo finfo = info.get(file);
+		if (finfo == null) {
+			finfo = new FileInfo(file.lastModified(), basis, file.length());
+			synchronized(info) {
+				info.put(file, finfo);
+			}
+			if (DEBUG) System.out.println("First info for " + file + ": " + finfo);
+			return finfo;
+		} else if (finfo.getLastQueried() + quantum < basis) {
+			// note: if a file no longer exists, these return distinct values
+			// that also appear as changes
+			finfo = new FileInfo(file.lastModified(), basis, file.length());
+			synchronized(info) {
+				info.put(file, finfo);
+			}
+			return finfo;
+		} else {
+			return finfo;
+		}
+	}
+	
+	/** 
+	 * Tell if the file's timestamp or size changed in the past quantum 
+	 * and update the record 
+	 * @return true if file size or timestamp changed.
+	 */
+	public boolean isChanged(File file) {
+		return isChanged(file, System.currentTimeMillis());
+	}
+	
+	/** 
+	 * Tell if the file's timestamp or size changed since the basis time,
+	 * or otherwise changed in the past quantum,  
+	 * and update the record.
+	 * <p>
+	 * If a file does not exist, we always consider it to have changed
+	 * (since otherwise, we cannot tell if a client saw the transition
+	 * from existing to not existing).  
+	 * @param basis the system time in ms from which to judge the change
+	 * @return true if file size or timestamp changed since basis, or
+	 * the file does not exist
+	 */
+	public boolean isChanged(File file, long basis) {
+		FileInfo finfo = info.get(file);
+		FileInfo newInfo = getFileInfo(file, basis);
+		if (finfo == newInfo)
+			return false;
+		
+		// update info
+		info.put(file, newInfo);
+		
+		return finfo.isChangedFrom(newInfo);
+	}
+	
+	/**
+	 * Force a refresh of the given file's status on the next #isChanged() call.
+	 * @param file
+	 */
+	public void refresh(File file) {
+		synchronized(info) {
+			info.remove(file);
+		}
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/com.nokia.cpp.utils.core/src/com/nokia/cpp/internal/api/utils/core/ExternalFileInfoCollection.java	Tue Jan 05 11:23:50 2010 -0600
@@ -0,0 +1,153 @@
+/*
+* Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies).
+* All rights reserved.
+* This component and the accompanying materials are made available
+* under the terms of the License "Eclipse Public License v1.0"
+* which accompanies this distribution, and is available
+* at the URL "http://www.eclipse.org/legal/epl-v10.html".
+*
+* Initial Contributors:
+* Nokia Corporation - initial contribution.
+*
+* Contributors:
+*
+* Description: 
+*
+*/
+
+package com.nokia.cpp.internal.api.utils.core;
+
+import java.io.File;
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import com.nokia.cpp.internal.api.utils.core.ExternalFileInfoCache.FileInfo;
+
+/**
+ * This object corrals a set of timestamp checks against a collection
+ * of related files (using an {@link ExternalFileInfoCache} for
+ * storage). 
+ * <p>
+ * Multiple instances of this object can (and should) share a cache.
+ * <p>
+ * As with {@link ExternalFileInfoCache}, we recommend using this 
+ * tracker only for files outside the Eclipse workspace.  For workspace 
+ * resources, use resource listeners to avoid polling for file changes.   
+ */
+public class ExternalFileInfoCollection implements Serializable {
+	
+	private static final long serialVersionUID = 3685308336593621639L;
+
+	// the files to check and the last info we found
+	private Map<File, ExternalFileInfoCache.FileInfo> fileMap = new HashMap<File, FileInfo>();
+	
+	// time in ms between checks of all the files
+	private long quantum;
+	
+	// last time we checked
+	private long lastCheckTime;
+
+	private ExternalFileInfoCache cache;
+	
+	protected ExternalFileInfoCollection() {
+		// for serialization
+	}
+	
+	/**
+	 * Create an object to check the states of related file timestamps and sizes.
+	 * @param quantumMs time in ms between checking all the files 
+	 */
+	public ExternalFileInfoCollection(ExternalFileInfoCache cache,
+			File[] files, long quantumMs) {
+		Check.checkArg(cache);
+		this.cache = cache;
+		this.quantum = quantumMs;
+		setFiles(files);
+		this.lastCheckTime = System.currentTimeMillis();
+	}
+	
+	/**
+	 * If the collection's time quantum has passed, check all the files 
+	 * to see if any have changed.
+	 * <p>
+	 * Note that by {@link ExternalFileInfoCache#isChanged(File, long)},
+	 * a deleted file always appears to have changed, so it is assumed that if a 
+	 * change is detected, a client will invoke {@link #setFiles(File[])} to 
+	 * ensure deleted files are removed from the collection.
+	 * @return true if any file in the collection has appeared to change
+	 * since the last quantum
+	 */
+	public synchronized boolean anyChanged() {
+		boolean changed = false;
+		if (System.currentTimeMillis() > lastCheckTime + quantum) {
+			for (File file : fileMap.keySet()) {
+				if (checkForChange(file)) {
+					changed = true;
+					// continue to check all the files, so we update the info uniformly
+				}
+			}
+			lastCheckTime = System.currentTimeMillis();
+		}
+		return changed;
+	}
+	
+	/**
+	 * Recheck one file in the collection and update data.
+	 * @param file
+	 * @return
+	 */
+	protected boolean checkForChange(File file) {
+		FileInfo currentInfo = fileMap.get(file);
+		FileInfo newInfo = cache.getFileInfo(file, lastCheckTime);
+		
+		fileMap.put(file, newInfo);
+		
+		return currentInfo == null || currentInfo.isChangedFrom(newInfo);
+	}
+
+	/**
+	 * Update the files incorporated in the timestamp/size check.  
+	 * @param files the files to set
+	 */
+	public synchronized void setFiles(File[] files) {
+		this.fileMap.clear();
+		
+		// prime the cache so every file's current timestamp and size is known
+		if (files != null) {
+			for (File file : files) {
+				checkForChange(file);
+			}
+		}
+	}
+	
+	/**
+	 * Get the files this collection checks.
+	 * @return the files
+	 */
+	public File[] getFiles() {
+		Set<File> files = fileMap.keySet();
+		return (File[]) files.toArray(new File[files.size()]);
+	}
+	
+	/**
+	 * Refresh the timestamps for the affected files, ensuring that
+	 * changes on disk are immediately detected for the next {@link #anyChanged()}
+	 * call.
+	 */
+	public synchronized void refresh() {
+		for (File file : fileMap.keySet()) {
+			cache.refresh(file);
+		}
+		this.lastCheckTime = 0;
+	}
+	
+	/**
+	 * Set the quantum for the timestamp collection.
+	 * @param quantumMs time is ms between checks of files
+	 */
+	public void setRecheckQuantum(long quantum) {
+		this.quantum = quantum;
+	}
+}
--- a/core/com.nokia.cpp.utils.core/src/com/nokia/cpp/internal/api/utils/core/Tuple.java	Tue Jan 05 11:22:47 2010 -0600
+++ b/core/com.nokia.cpp.utils.core/src/com/nokia/cpp/internal/api/utils/core/Tuple.java	Tue Jan 05 11:23:50 2010 -0600
@@ -16,13 +16,20 @@
 */
 package com.nokia.cpp.internal.api.utils.core;
 
+import java.io.Serializable;
+
 /**
  * A tuple of zero or more items.
  *
  */
-public class Tuple {
+public class Tuple implements Serializable {
+	private static final long serialVersionUID = -3117335085610864101L;
+	
 	private Object[] args;
 
+	protected Tuple() {
+		// for serialization
+	}
 	public Tuple(Object... args) {
 		this.args = args;
 	}
--- a/project/com.nokia.carbide.cpp.epoc.engine/src/com/nokia/carbide/cpp/epoc/engine/EpocEnginePlugin.java	Tue Jan 05 11:22:47 2010 -0600
+++ b/project/com.nokia.carbide.cpp.epoc.engine/src/com/nokia/carbide/cpp/epoc/engine/EpocEnginePlugin.java	Tue Jan 05 11:23:50 2010 -0600
@@ -29,6 +29,8 @@
 import com.nokia.carbide.cpp.epoc.engine.model.sbv.*;
 import com.nokia.carbide.internal.cpp.epoc.engine.model.SBVModelFactory;
 import com.nokia.carbide.internal.cpp.epoc.engine.model.ViewDataCache;
+import com.nokia.cpp.internal.api.utils.core.ExternalFileInfoCache;
+import com.nokia.cpp.internal.api.utils.core.ExternalFileInfoCollection;
 import com.nokia.cpp.internal.api.utils.core.Logging;
 import com.nokia.cpp.internal.api.utils.core.MultiResourceChangeListenerDispatcher;
 
@@ -61,6 +63,10 @@
 	private static ViewDataCache<IBldInfOwnedModel, IBldInfModel, IBldInfView, IBldInfData> bldInfViewDataCache;
 
 	private static ViewDataCache<IImageMakefileOwnedModel, IImageMakefileModel, IImageMakefileView, IImageMakefileData> imageMakefileViewDataCache;
+
+	private static ExternalFileInfoCache externalFileTimestampSizeCache = 
+		new ExternalFileInfoCache();
+	
 	/**
 	 * The constructor.
 	 */
@@ -506,6 +512,16 @@
 		
 		return runnable.run(data);
 	}
+
+	/**
+	 * Get the global cache of file timestamps and sizes associated with the EPOC engine.
+	 * This is used to track the status of files in SDKs which are unlikely to change
+	 * often and for which we don't want to waste OS time checking over and over. 
+	 * @return {@link ExternalFileInfoCache}
+	 */
+	public static ExternalFileInfoCache getExternalFileInfoCache() {
+		return externalFileTimestampSizeCache;
+	}
 	
 
 }
--- a/project/com.nokia.carbide.cpp.epoc.engine/src/com/nokia/carbide/cpp/epoc/engine/preprocessor/DefaultTranslationUnitProvider.java	Tue Jan 05 11:22:47 2010 -0600
+++ b/project/com.nokia.carbide.cpp.epoc.engine/src/com/nokia/carbide/cpp/epoc/engine/preprocessor/DefaultTranslationUnitProvider.java	Tue Jan 05 11:23:50 2010 -0600
@@ -42,7 +42,7 @@
 	private boolean DUMP = false;
 	
 	/** count of entries allowed */
-	private static final int DEFAULT_MAX_CACHE_SIZE = 32;
+	private static final int DEFAULT_MAX_CACHE_SIZE = 256;
 	/** the minimum number of hits (accesses) to the TU to keep it when flushing cache. */
 	private static final int DEFAULT_MINIMUM_HITS_TO_KEEP = 8;
 	
@@ -156,7 +156,8 @@
 		if (DUMP)
 			System.out.println("Releasing TU for " + file); //$NON-NLS-1$
 		tuCache.remove(file);
-		cacheHits.remove(file);
+		// do not lose info about file importance
+		//cacheHits.remove(file);
 		cacheTimes.remove(file);
 		cacheOrder.remove(file);
 	}
--- a/project/com.nokia.carbide.cpp.epoc.engine/src/com/nokia/carbide/cpp/epoc/engine/preprocessor/IDefine.java	Tue Jan 05 11:22:47 2010 -0600
+++ b/project/com.nokia.carbide.cpp.epoc.engine/src/com/nokia/carbide/cpp/epoc/engine/preprocessor/IDefine.java	Tue Jan 05 11:23:50 2010 -0600
@@ -17,11 +17,13 @@
 
 package com.nokia.carbide.cpp.epoc.engine.preprocessor;
 
+import java.io.Serializable;
+
 /**
  * High-level information about a macro definition.
  *
  */
-public interface IDefine {
+public interface IDefine extends Serializable {
 	/** 
 	 * Get the macro name (never null)
 	 */
--- a/project/com.nokia.carbide.cpp.epoc.engine/src/com/nokia/carbide/internal/api/cpp/epoc/engine/preprocessor/MacroScanner.java	Tue Jan 05 11:22:47 2010 -0600
+++ b/project/com.nokia.carbide.cpp.epoc.engine/src/com/nokia/carbide/internal/api/cpp/epoc/engine/preprocessor/MacroScanner.java	Tue Jan 05 11:23:50 2010 -0600
@@ -96,13 +96,16 @@
 		if (DUMP)
 			System.out.println("Scanning #defines for " + file); //$NON-NLS-1$
 		
-		includes.add(file);
-		
 		ITranslationUnit tu_ = tuProvider.getTranslationUnit(file, documentProvider);
 		if (!(tu_ instanceof IASTTranslationUnit)) {
 			EpocEnginePlugin.log(new FileNotFoundException("Failed to scan #defines for " + file)); //$NON-NLS-1$
 			return;
 		}
+
+		// only add file if it exists, otherwise, its absence makes clients
+		// think something has changed
+		includes.add(file);
+
 		IASTTranslationUnit tu = (IASTTranslationUnit) tu_;
 		for (IASTTopLevelNode node : tu.getNodes()) {
 			if (node instanceof IASTPreprocessorDefineStatement) {
@@ -163,8 +166,9 @@
 	}
 	
 	/**
-	 * Get the list of all header files that were scanned
-	 * @return
+	 * Get the list of all header files that were scanned (includes the
+	 * top level file)
+	 * @return non-<code>null</code> array of files
 	 */
 	public File[] getIncludedFiles() {
 		return includes.toArray(new File[includes.size()]);
--- a/project/com.nokia.carbide.cpp.epoc.engine/src/com/nokia/carbide/internal/cpp/epoc/engine/model/ViewDataCache.java	Tue Jan 05 11:22:47 2010 -0600
+++ b/project/com.nokia.carbide.cpp.epoc.engine/src/com/nokia/carbide/internal/cpp/epoc/engine/model/ViewDataCache.java	Tue Jan 05 11:23:50 2010 -0600
@@ -17,6 +17,7 @@
 
 package com.nokia.carbide.internal.cpp.epoc.engine.model;
 
+import com.nokia.carbide.cpp.epoc.engine.EpocEnginePlugin;
 import com.nokia.carbide.cpp.epoc.engine.model.*;
 import com.nokia.carbide.cpp.epoc.engine.preprocessor.*;
 import com.nokia.cpp.internal.api.utils.core.*;
@@ -38,6 +39,7 @@
 public class ViewDataCache<OwnedModel extends IOwnedModel, Model extends IModel, View extends IView, Data extends IData<View>> {
 
 	public static boolean DEBUG = false;
+	public static boolean DEBUG_VERBOSE = false;
 	
 	/** A key which can retrieve the current state of a model for a given filter. */
 	static class ViewConfigKey extends Pair<IPath, IViewFilter> {
@@ -60,7 +62,7 @@
 		 */
 		/*private*/ Object getMD5(String uniqueKey) {
 			try {
-				MessageDigest md = MessageDigest.getInstance("MD5");
+				MessageDigest md = MessageDigest.getInstance("MD5"); //$NON-NLS-1$
 				return md.digest(uniqueKey.getBytes());
 			} catch (NoSuchAlgorithmException e) {
 				return uniqueKey;
@@ -114,7 +116,7 @@
 			IViewParserConfiguration parserConfig = viewConfiguration.getViewParserConfiguration();
 			String includeState = getIncludeState(parserConfig.getIncludeFileLocator());
 			String projectState = parserConfig.getProjectLocation().toOSString();
-			return filterState + "/" + projectState + "/" + includeState + "/" + macroState;
+			return filterState + "/" + projectState + "/" + includeState + "/" + macroState; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
 		}
 
 		/**
@@ -164,110 +166,12 @@
 		
 	}
 
-	static class FileTimestampSizeCollection {
-		static FileTimestampSizeCollection INSTANCE = new FileTimestampSizeCollection();
-		/** Check timestamps only once in this number of milliseconds */
-		final int QUANTUM = 0;
-		
-		static class FileInfo extends Tuple {
-			public FileInfo(long lastModified, long lastQueried, long size) {
-				super(lastModified, lastQueried, size);
-			}
-			public long getLastModified() {
-				return (Long) get(0);
-			}
-			public long getLastQueried() {
-				return (Long) get(0);
-			}
-			public long getLength() { 
-				return (Long) get(2);
-			}
-		}
-		/** map of file to file size + last queried timestamp + time of last query 
-		 * (use File, not IPath, so we canonicalize for the OS) */
-		private Map<File, FileInfo> info = new HashMap<File, FileInfo>();		
-		
-		
-		/** Tell if the file's timestamp changed in the past quantum 
-		 * and update the record */
-		public boolean changed(File file) {
-			long now = System.currentTimeMillis();
-			FileInfo finfo = info.get(file);
-			if (finfo == null) {
-				finfo = new FileInfo(file.lastModified(), now, file.length());
-				info.put(file, finfo);
-				if (DEBUG) System.out.println("First info for " + file + ": " + finfo);
-				return true;
-			} else if (finfo.getLastQueried() + QUANTUM < now) {
-				// don't check times more than QUANTUM
-				long origTime = finfo.getLastModified();
-				long origSize = finfo.getLength();
-				finfo = new FileInfo(file.lastModified(), now, file.length());
-				info.put(file, finfo);
-				if (DEBUG) System.out.println("Updated info for " + file + ": " + origTime + "/" + origSize + " <=> " 
-						+ finfo.getLastModified() + "/" + finfo.getLength());
-				return origTime != finfo.getLastModified() || finfo.getLastModified() == 0 // 0 if deleted
-					|| origSize != finfo.getLength();		
-			} else {
-				// not changed, as far as we assume
-				if (DEBUG) System.out.println("Assuming no change for " + file);
-				return false;
-			}
-		}
-	}
-	
-	public static class ModelFileTimestampCollection {
-		/**
-		 * Delay in ms between successive checks of the filesystem, to avoid wasting time
-		 * when such checks are slow, and in cases where it's unlikely the human will edit files
-		 * fast enough to care.
-		 */
-		public static final long QUANTUM = HostOS.IS_WIN32 ? 50 : 10;
-		private File[] files;
-		private long lastQuery;
-
-		public ModelFileTimestampCollection(IView view) {
-			IPath[] paths = view.getReferencedFiles();
-			this.files = new File[paths.length];
-			int idx = 0;
-			for (IPath path : paths) {
-				files[idx] = path.toFile();
-				// prime the cache
-				FileTimestampSizeCollection.INSTANCE.changed(files[idx]);
-				idx++;
-			}
-			this.lastQuery = System.currentTimeMillis();
-		}
-
-		/**
-		 * Tell if any of the files have changed
-		 * @return
-		 */
-		public boolean changed() {
-			long prevQuery = lastQuery;
-			lastQuery = System.currentTimeMillis();
-			
-			// don't check more often than the resolution of the file allows
-			if (prevQuery + QUANTUM > lastQuery) {
-				if (DEBUG) System.out.println("Skipping fileinfo check");
-				return false;
-			}
-			
-			for (File file : files) {
-				if (FileTimestampSizeCollection.INSTANCE.changed(file)) {
-					return true;
-				}
-			}
-			return false;
-		}
-	}
-	
 	/** the minimum number of hits (accesses) to the entry to keep it when flushing cache. */
 	private static final int DEFAULT_MINIMUM_HITS_TO_KEEP = 8;
 
 	private IModelProvider<OwnedModel, Model> modelProvider;
 	private Map<ViewConfigKey, Pair<ViewConfigState, Data>> cachedData;
-	private Map<ViewConfigKey, ModelFileTimestampCollection> cachedTimestamps;
+	private Map<ViewConfigKey, ExternalFileInfoCollection> cachedFileInfo;
 	private Map<ViewConfigKey, Integer> cacheHits;
 	private List<ViewConfigKey> cacheOrder;
 
@@ -281,7 +185,7 @@
 		
 		this.modelProvider = provider;
 		this.cachedData = new HashMap<ViewConfigKey, Pair<ViewConfigState,Data>>();
-		this.cachedTimestamps = new HashMap<ViewConfigKey, ModelFileTimestampCollection>();
+		this.cachedFileInfo = new HashMap<ViewConfigKey, ExternalFileInfoCollection>();
 		this.cacheHits = new HashMap<ViewConfigKey, Integer>();
 		this.cacheOrder = new LinkedList<ViewConfigKey>();
 	}
@@ -366,11 +270,11 @@
 		}
 		statefulData = cachedData.get(key);
 		if (statefulData != null) {
-			// now check that the file timestamps are valid.
-			ModelFileTimestampCollection timestamps = cachedTimestamps.get(key);
-			if (timestamps.changed()) {
+			// now check that the file info is valid.
+			ExternalFileInfoCollection fileinfo = cachedFileInfo.get(key);
+			if (fileinfo.anyChanged()) {
 				if (DEBUG) {
-					System.out.println("One or more relevant files changed for " + modelPath);
+					System.out.println("One or more relevant files changed for " + modelPath); //$NON-NLS-1$
 				}
 				removeAllEntriesForModel(modelPath);
 				statefulData = null;
@@ -380,9 +284,9 @@
 					if (DEBUG) {
 						String orig = statefulData.first.toString();
 						String curr = state.toString();
-						System.out.println("State changed (from:\n" 
-								+ orig.substring(0, Math.min(100, orig.length())) + "\nto:\n" + 
-								curr.substring(0, Math.min(100, curr.length())) + ")\n");
+						System.out.println("State changed (from:\n" //$NON-NLS-1$
+								+ orig.substring(0, Math.min(100, orig.length())) + "\nto:\n" + //$NON-NLS-1$ 
+								curr.substring(0, Math.min(100, curr.length())) + ")\n"); //$NON-NLS-1$
 					}
 					removeEntry(key);
 					cachedData.remove(key);
@@ -392,8 +296,8 @@
 		}
 		
 		if (statefulData != null) {
-			if (DEBUG) {
-				System.out.println("Found entry for " + key);
+			if (DEBUG_VERBOSE) {
+				System.out.println("Found entry for " + key); //$NON-NLS-1$
 			}
 			cacheHits.put(key, cacheHits.get(key) + 1);
 			data = statefulData.second;
@@ -422,7 +326,7 @@
 			ViewConfigState state, ViewConfigKey key) throws CoreException {
 		Data data;
 		if (DEBUG) {
-			System.out.println("Fetching view data for " + key);
+			System.out.println("Fetching view data for " + key); //$NON-NLS-1$
 		}
 		Model model = modelProvider.getSharedModel(modelPath);
 		if (model == null)
@@ -437,11 +341,21 @@
 				if (data == null)
 					return null;
 				
-				ModelFileTimestampCollection timestamps = new ModelFileTimestampCollection(view);
+				IPath[] referencedFiles = view.getReferencedFiles();
+				File[] files = new File[referencedFiles.length];
+				for (int idx = 0; idx < referencedFiles.length; idx++) {
+					files[idx] = referencedFiles[idx].toFile();
+				}
+				
+				ExternalFileInfoCollection fileinfo = 
+					new ExternalFileInfoCollection(
+							EpocEnginePlugin.getExternalFileInfoCache(),
+							files,
+							FileUtils.getMinimumFileTimestampResolution(modelPath));
 				synchronized (cachedData) {
 					// the data may have already been registered... oh well
 					cachedData.put(key, new Pair<ViewConfigState, Data>(state, data));
-					cachedTimestamps.put(key, timestamps);
+					cachedFileInfo.put(key, fileinfo);
 					cacheOrder.add(0, key);
 					cacheHits.put(key, 0);
 				}
@@ -470,8 +384,10 @@
 	 * @param key
 	 */
 	private void removeEntry(ViewConfigKey key) {
-		cachedTimestamps.remove(key);
-		cacheHits.remove(key);
+		cachedFileInfo.remove(key);
+		
+		// do not lose info about file importance
+		//cacheHits.remove(key);
 		cacheOrder.remove(key);
 		cachedData.remove(key);
 	}
@@ -497,7 +413,7 @@
 			Integer hits = cacheHits.get(key);
 			if (hits == null || hits < minimumHitsToKeep) {
 				if (DEBUG) {
-					System.out.println("*** Flushing " + key);
+					System.out.println("*** Flushing " + key); //$NON-NLS-1$
 				}
 				removeEntry(key);
 				toRemove--;
@@ -512,12 +428,11 @@
 			if (key.equals(retainKey))
 				continue;
 			if (DEBUG) {
-				System.out.println("*** Flushing " + key);
+				System.out.println("*** Flushing " + key); //$NON-NLS-1$
 			}
 			removeEntry(key);
 			toRemove--;
 		}
 
 	}
-	
 }