forked from Misaki/GhostEngine
Improve AssetDatabase performance.
This commit is contained in:
@@ -5,7 +5,7 @@ namespace Ghost.Editor.Core.AssetHandle;
|
||||
|
||||
public static partial class AssetDatabase
|
||||
{
|
||||
private static readonly Dictionary<Type, object> s_importerInstances = new();
|
||||
private static readonly Dictionary<Type, AssetImporter> s_importerInstances = new();
|
||||
|
||||
/// <summary>
|
||||
/// Import an asset at the specified path.
|
||||
@@ -25,8 +25,8 @@ public static partial class AssetDatabase
|
||||
// Get or create importer instance
|
||||
if (!s_importerInstances.TryGetValue(importerType, out var importerInstance))
|
||||
{
|
||||
importerInstance = Activator.CreateInstance(importerType);
|
||||
if (importerInstance == null)
|
||||
importerInstance = Activator.CreateInstance(importerType) as AssetImporter;
|
||||
if (importerInstance is null)
|
||||
{
|
||||
return Result.Failure($"Failed to create importer instance for type {importerType.Name}");
|
||||
}
|
||||
@@ -41,45 +41,7 @@ public static partial class AssetDatabase
|
||||
return Result.Failure($"Failed to read asset metadata: {metaResult.Message}");
|
||||
}
|
||||
|
||||
// TODO: Avoid reflection.
|
||||
// Find and invoke the ImportAsync method. Support importers that accept (string, AssetMeta)
|
||||
// or (string, AssetMeta, CancellationToken).
|
||||
var importMethod = importerType.GetMethod("ImportAsync", BindingFlags.Public | BindingFlags.Instance);
|
||||
if (importMethod == null)
|
||||
{
|
||||
return Result.Failure($"ImportAsync method not found on importer {importerType.Name}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var parameters = importMethod.GetParameters();
|
||||
object? invokeResult;
|
||||
|
||||
if (parameters.Length == 2)
|
||||
{
|
||||
invokeResult = importMethod.Invoke(importerInstance, new object[] { assetPath, metaResult.Value });
|
||||
}
|
||||
else if (parameters.Length == 3 && parameters[2].ParameterType == typeof(CancellationToken))
|
||||
{
|
||||
invokeResult = importMethod.Invoke(importerInstance, new object[] { assetPath, metaResult.Value, token });
|
||||
}
|
||||
else
|
||||
{
|
||||
return Result.Failure($"Unsupported ImportAsync signature on importer {importerType.Name}");
|
||||
}
|
||||
|
||||
if (invokeResult is not Task<Result> task)
|
||||
{
|
||||
return Result.Failure("Importer did not return a valid Task<Result>");
|
||||
}
|
||||
|
||||
var result = await task;
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure($"Asset import failed: {ex.Message}");
|
||||
}
|
||||
return await importerInstance.ImportAsync(assetPath, metaResult.Value, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -122,8 +84,8 @@ public static partial class AssetDatabase
|
||||
// Get or create importer instance
|
||||
if (!s_importerInstances.TryGetValue(importerType, out var importerInstance))
|
||||
{
|
||||
importerInstance = Activator.CreateInstance(importerType);
|
||||
if (importerInstance == null)
|
||||
importerInstance = Activator.CreateInstance(importerType) as AssetImporter;
|
||||
if (importerInstance is null)
|
||||
{
|
||||
return Result<Guid>.Failure($"Failed to create importer instance for type {importerType.Name}");
|
||||
}
|
||||
@@ -138,53 +100,29 @@ public static partial class AssetDatabase
|
||||
return Result<Guid>.Failure($"ExportAsync method not found on importer {importerType.Name}. This importer does not support exporting.");
|
||||
}
|
||||
|
||||
try
|
||||
// Generate metadata for the new asset
|
||||
var result = await GenerateMetaFileAsync(assetPath, token);
|
||||
if (result.IsFailure)
|
||||
{
|
||||
// Generate metadata for the new asset
|
||||
await GenerateMetaFileAsync(assetPath, token);
|
||||
|
||||
var metaResult = await ReadMetaFileAsync(assetPath, token);
|
||||
if (metaResult.IsFailure)
|
||||
{
|
||||
return Result<Guid>.Failure($"Failed to generate metadata: {metaResult.Message}");
|
||||
}
|
||||
|
||||
var parameters = exportMethod.GetParameters();
|
||||
object? invokeResult;
|
||||
|
||||
if (parameters.Length == 3)
|
||||
{
|
||||
invokeResult = exportMethod.Invoke(importerInstance, new object[] { assetPath, assetData, metaResult.Value });
|
||||
}
|
||||
else if (parameters.Length == 4 && parameters[3].ParameterType == typeof(CancellationToken))
|
||||
{
|
||||
invokeResult = exportMethod.Invoke(importerInstance, new object[] { assetPath, assetData, metaResult.Value, token });
|
||||
}
|
||||
else
|
||||
{
|
||||
return Result<Guid>.Failure($"Unsupported ExportAsync signature on importer {importerType.Name}");
|
||||
}
|
||||
|
||||
if (invokeResult is not Task<Result> task)
|
||||
{
|
||||
return Result<Guid>.Failure("Exporter did not return a valid Task<Result>");
|
||||
}
|
||||
|
||||
var result = await task;
|
||||
if (result.IsFailure)
|
||||
{
|
||||
return Result<Guid>.Failure(result.Message);
|
||||
}
|
||||
|
||||
// Calculate file hash and update database
|
||||
var fileHash = await CalculateFileHashAsync(assetPath, token);
|
||||
await UpsertAssetAsync(assetPath, metaResult.Value, fileHash, null, token);
|
||||
|
||||
return metaResult.Value.Guid;
|
||||
return Result<Guid>.Failure($"Failed to generate metadata: {result.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
var metaResult = await ReadMetaFileAsync(assetPath, token);
|
||||
if (metaResult.IsFailure)
|
||||
{
|
||||
return Result<Guid>.Failure($"Asset export failed: {ex.Message}");
|
||||
return Result<Guid>.Failure($"Failed to read metadata: {metaResult.Message}");
|
||||
}
|
||||
|
||||
result = await importerInstance.ExportAsync(assetPath, assetData, metaResult.Value, token);
|
||||
if (result.IsFailure)
|
||||
{
|
||||
return Result<Guid>.Failure(result.Message);
|
||||
}
|
||||
|
||||
// Calculate file hash and update database
|
||||
var fileHash = await CalculateFileHashAsync(assetPath, token);
|
||||
await UpsertAssetAsync(assetPath, metaResult.Value, fileHash, null, token);
|
||||
|
||||
return metaResult.Value.Guid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ using Ghost.Core;
|
||||
using Ghost.Editor.Core.Utilities;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandle;
|
||||
|
||||
@@ -180,7 +180,7 @@ public static partial class AssetDatabase
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to load asset cache: {ex.Message}");
|
||||
Logger.LogError($"Failed to load asset cache: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,8 @@ public static partial class AssetDatabase
|
||||
private static bool s_autoRefreshEnabled = true;
|
||||
private static readonly Queue<AssetCommand> s_waitingCommands = new(); // Commands waiting for manual refresh
|
||||
|
||||
private static TaskCompletionSource<bool>? s_refreshTcs;
|
||||
|
||||
// Initialization guard
|
||||
private static readonly Lock s_initializationLock = new();
|
||||
private static bool s_initialized = false;
|
||||
@@ -77,7 +79,7 @@ public static partial class AssetDatabase
|
||||
/// Initialize the asset database.
|
||||
/// Must be called after project is loaded.
|
||||
/// </summary>
|
||||
internal static async void Initialize(CancellationToken token = default)
|
||||
internal static async Task Initialize(CancellationToken token = default)
|
||||
{
|
||||
lock (s_initializationLock)
|
||||
{
|
||||
@@ -95,7 +97,6 @@ public static partial class AssetDatabase
|
||||
|
||||
AssetsDirectory = new DirectoryInfo(Path.Combine(Path.GetDirectoryName(ProjectService.CurrentProject.Path)!, ProjectService.ASSETS_FOLDER));
|
||||
|
||||
// Initialize command channel (unbounded for simplicity)
|
||||
s_commandChannel = Channel.CreateUnbounded<AssetCommand>(new UnboundedChannelOptions
|
||||
{
|
||||
SingleReader = false, // Timer callback reads
|
||||
@@ -105,13 +106,9 @@ public static partial class AssetDatabase
|
||||
// Initialize command processor timer (starts disabled, triggered by events)
|
||||
s_commandProcessorTimer = new Timer(ProcessPendingCommands, null, Timeout.Infinite, Timeout.Infinite);
|
||||
|
||||
// Initialize database
|
||||
await InitializeDatabaseAsync(token);
|
||||
|
||||
// Load asset cache from database
|
||||
await LoadAssetCacheFromDatabaseAsync(token);
|
||||
|
||||
// Initialize file system watcher
|
||||
s_watcher = new FileSystemWatcher
|
||||
{
|
||||
Path = AssetsDirectory.FullName,
|
||||
@@ -123,7 +120,6 @@ public static partial class AssetDatabase
|
||||
InitializeAssetHandle();
|
||||
InitializeMetaData();
|
||||
|
||||
// Validate and fix database on startup
|
||||
await ValidateAndFixDatabaseAsync(token);
|
||||
}
|
||||
|
||||
@@ -193,16 +189,17 @@ public static partial class AssetDatabase
|
||||
{
|
||||
s_commandChannel?.Writer.TryWrite(cmd);
|
||||
}
|
||||
|
||||
s_refreshTcs = new TaskCompletionSource<bool>();
|
||||
}
|
||||
|
||||
// Post manual refresh command
|
||||
s_commandChannel?.Writer.TryWrite(new AssetCommand(AssetCommandType.ManualRefresh, string.Empty));
|
||||
|
||||
// Trigger timer immediately
|
||||
s_commandProcessorTimer?.Change(TimeSpan.Zero, Timeout.InfiniteTimeSpan);
|
||||
|
||||
// Wait a bit for processing to complete (this is best-effort)
|
||||
await Task.Delay(200, token);
|
||||
if (!await s_refreshTcs.Task.WaitAsync(token))
|
||||
{
|
||||
return Result.Failure("Asset database refresh failed");
|
||||
}
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
@@ -328,7 +325,7 @@ public static partial class AssetDatabase
|
||||
/// <summary>
|
||||
/// Timer callback to process pending commands.
|
||||
/// </summary>
|
||||
private static void ProcessPendingCommands(object? state)
|
||||
private static async void ProcessPendingCommands(object? state)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -347,32 +344,38 @@ public static partial class AssetDatabase
|
||||
commandsByPath[cmd.Path] = cmd;
|
||||
}
|
||||
|
||||
// Filter out temp files (files that were created then deleted)
|
||||
lock (s_commandLock)
|
||||
{
|
||||
var pathsToProcess = commandsByPath.Keys.ToList();
|
||||
foreach (var path in pathsToProcess)
|
||||
{
|
||||
// If file was created/modified but doesn't exist anymore, skip
|
||||
if (!File.Exists(path) && commandsByPath[path].Type != AssetCommandType.FileDeleted)
|
||||
{
|
||||
commandsByPath.Remove(path);
|
||||
}
|
||||
}
|
||||
// NOTE: We handle the temp file filtering in each command handler now
|
||||
// We should able to remove this allocation heavy code
|
||||
|
||||
// Clear pending paths
|
||||
s_pendingCommandPaths.Clear();
|
||||
}
|
||||
// Filter out temp files (files that were created then deleted)
|
||||
// lock (s_commandLock)
|
||||
// {
|
||||
// var pathsToProcess = commandsByPath.Keys.ToList();
|
||||
// foreach (var path in pathsToProcess)
|
||||
// {
|
||||
// // If file was created/modified but doesn't exist anymore, skip
|
||||
// if (!File.Exists(path) && commandsByPath[path].Type != AssetCommandType.FileDeleted)
|
||||
// {
|
||||
// commandsByPath.Remove(path);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Clear pending paths
|
||||
// s_pendingCommandPaths.Clear();
|
||||
// }
|
||||
|
||||
// Execute commands
|
||||
foreach (var cmd in commandsByPath.Values)
|
||||
{
|
||||
ExecuteCommandAsync(cmd).GetAwaiter().GetResult();
|
||||
await ExecuteCommandAsync(cmd);
|
||||
}
|
||||
|
||||
s_refreshTcs?.SetResult(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error processing commands: {ex.Message}");
|
||||
Logger.LogError($"Error processing commands: {ex.Message}");
|
||||
s_refreshTcs?.SetResult(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,6 +416,11 @@ public static partial class AssetDatabase
|
||||
/// </summary>
|
||||
private static async Task HandleFileCreatedAsync(string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await GenerateMetaFileAsync(path, CancellationToken.None);
|
||||
}
|
||||
|
||||
@@ -421,6 +429,11 @@ public static partial class AssetDatabase
|
||||
/// </summary>
|
||||
private static async Task HandleFileModifiedAsync(string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if file hash changed
|
||||
var metaResult = await ReadMetaFileAsync(path, CancellationToken.None);
|
||||
if (metaResult.IsFailure)
|
||||
@@ -447,6 +460,11 @@ public static partial class AssetDatabase
|
||||
/// </summary>
|
||||
private static async Task HandleFileDeletedAsync(string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var metaFileResult = GetMetaFilePath(path);
|
||||
if (metaFileResult.IsSuccess && File.Exists(metaFileResult.Value))
|
||||
{
|
||||
@@ -468,7 +486,7 @@ public static partial class AssetDatabase
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error deleting asset metadata: {ex.Message}");
|
||||
Logger.LogError($"Error deleting asset metadata: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -478,6 +496,17 @@ public static partial class AssetDatabase
|
||||
/// </summary>
|
||||
private static async Task HandleFileRenamedAsync(string oldPath, string newPath)
|
||||
{
|
||||
if (!File.Exists(oldPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (File.Exists(newPath))
|
||||
{
|
||||
Logger.LogWarning($"Cannot rename asset from '{oldPath}' to '{newPath}': target file already exists.");
|
||||
return;
|
||||
}
|
||||
|
||||
var oldMetaPath = oldPath + Utilities.FileExtensions.META_FILE_EXTENSION;
|
||||
var newMetaPath = newPath + Utilities.FileExtensions.META_FILE_EXTENSION;
|
||||
|
||||
|
||||
@@ -2,21 +2,16 @@ using Ghost.Core;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandle;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all asset importers.
|
||||
/// Asset importers process source files and convert them into engine-ready formats.
|
||||
/// </summary>
|
||||
/// <typeparam name="TSettings">The type of importer settings this importer uses.</typeparam>
|
||||
internal abstract class AssetImporter<TSettings>
|
||||
where TSettings : ImporterSettings, new()
|
||||
public abstract class AssetImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// Import the asset at the specified path with the given settings.
|
||||
/// </summary>
|
||||
/// <param name="assetPath">Full path to the source asset file.</param>
|
||||
/// <param name="meta">Metadata for the asset.</param>
|
||||
/// <param name="token">Cancellation token.</param>
|
||||
/// <returns>Result indicating success or failure.</returns>
|
||||
public abstract Task<Result> ImportAsync(string assetPath, AssetMeta meta);
|
||||
public abstract ValueTask<Result> ImportAsync(string assetPath, AssetMeta meta, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Export in-memory asset data to disk.
|
||||
@@ -26,31 +21,12 @@ internal abstract class AssetImporter<TSettings>
|
||||
/// <param name="assetPath">Full path where the asset should be saved.</param>
|
||||
/// <param name="assetData">In-memory asset data to serialize.</param>
|
||||
/// <param name="meta">Metadata for the asset.</param>
|
||||
/// <param name="token">Cancellation token.</param>
|
||||
/// <returns>Result indicating success or failure.</returns>
|
||||
public virtual Task<Result> ExportAsync<T>(string assetPath, T assetData, AssetMeta meta) where T : class
|
||||
public virtual ValueTask<Result> ExportAsync<T>(string assetPath, T assetData, AssetMeta meta, CancellationToken token = default)
|
||||
where T : class
|
||||
{
|
||||
return Task.FromResult(Result.Failure("This importer does not support exporting assets."));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the settings for this importer from the metadata.
|
||||
/// Creates default settings if none exist.
|
||||
/// </summary>
|
||||
/// <param name="meta">Asset metadata.</param>
|
||||
/// <returns>The importer settings.</returns>
|
||||
protected TSettings GetSettings(AssetMeta meta)
|
||||
{
|
||||
var typeName = GetType().Name;
|
||||
var settings = meta.GetImporterSettings<TSettings>(typeName);
|
||||
|
||||
if (settings != null)
|
||||
{
|
||||
return settings;
|
||||
}
|
||||
|
||||
var defaultSettings = new TSettings();
|
||||
meta.SetImporterSettings(typeName, defaultSettings);
|
||||
return defaultSettings;
|
||||
return ValueTask.FromResult(Result.Failure("This importer does not support exporting assets."));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -78,3 +54,28 @@ internal abstract class AssetImporter<TSettings>
|
||||
return ValueTask.FromResult(Result.Success());
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class AssetImporter<TSettings> : AssetImporter
|
||||
where TSettings : ImporterSettings, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the settings for this importer from the metadata.
|
||||
/// Creates default settings if none exist.
|
||||
/// </summary>
|
||||
/// <param name="meta">Asset metadata.</param>
|
||||
/// <returns>The importer settings.</returns>
|
||||
protected TSettings GetSettings(AssetMeta meta)
|
||||
{
|
||||
var typeName = GetType().Name;
|
||||
var settings = meta.GetImporterSettings<TSettings>(typeName);
|
||||
|
||||
if (settings != null)
|
||||
{
|
||||
return settings;
|
||||
}
|
||||
|
||||
var defaultSettings = new TSettings();
|
||||
meta.SetImporterSettings(typeName, defaultSettings);
|
||||
return defaultSettings;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Ghost.Editor.Core.AssetHandle;
|
||||
/// Contains GUID, version, tags, and importer settings.
|
||||
/// FileHash and Dependencies are stored in the database only, not in .gmeta files.
|
||||
/// </summary>
|
||||
internal class AssetMeta
|
||||
public class AssetMeta
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for the asset.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
namespace Ghost.Editor.Core.AssetHandle;
|
||||
|
||||
internal abstract class ImporterSettings
|
||||
public abstract class ImporterSettings
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user