Fix replay menu loading time by using a cache file
Reviewed by: elexis Fixes #3433 Differential Revision: https://code.wildfiregames.com/D39 This was SVN commit r19674.
This commit is contained in:
parent
d096c2f09c
commit
80635665f7
@ -127,6 +127,12 @@ function showReplaySummary()
|
||||
});
|
||||
}
|
||||
|
||||
function reloadCache()
|
||||
{
|
||||
let selected = Engine.GetGUIObjectByName("replaySelection").selected;
|
||||
loadReplays(selected > -1 ? createReplaySelectionData(g_ReplaysFiltered[selected].directory) : "", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback.
|
||||
*/
|
||||
|
@ -59,7 +59,7 @@ function init(data)
|
||||
return;
|
||||
}
|
||||
|
||||
loadReplays(data && data.replaySelectionData);
|
||||
loadReplays(data && data.replaySelectionData, false);
|
||||
|
||||
if (!g_Replays)
|
||||
{
|
||||
@ -75,10 +75,13 @@ function init(data)
|
||||
* Store the list of replays loaded in C++ in g_Replays.
|
||||
* Check timestamp and compatibility and extract g_Playernames, g_MapNames, g_VictoryConditions.
|
||||
* Restore selected filters and item.
|
||||
* @param replaySelectionData - Currently selected filters and item to be restored after the loading.
|
||||
* @param compareFiles - If true, compares files briefly (which might be slow with optical harddrives),
|
||||
* otherwise blindly trusts the replay cache.
|
||||
*/
|
||||
function loadReplays(replaySelectionData)
|
||||
function loadReplays(replaySelectionData, compareFiles)
|
||||
{
|
||||
g_Replays = Engine.GetReplays();
|
||||
g_Replays = Engine.GetReplays(compareFiles);
|
||||
|
||||
if (!g_Replays)
|
||||
return;
|
||||
|
@ -249,6 +249,13 @@
|
||||
<action on="Press">deleteReplayButtonPressed();</action>
|
||||
</object>
|
||||
|
||||
<!-- Reload Cache Button -->
|
||||
<object type="button" style="StoneButton" size="40%+25 0 57%+25 100%">
|
||||
<translatableAttribute id="caption">Reload Cache</translatableAttribute>
|
||||
<translatableAttribute id="tooltip">Rebuild the replay cache from scratch. Potentially slow!</translatableAttribute>
|
||||
<action on="Press">reloadCache();</action>
|
||||
</object>
|
||||
|
||||
<!-- Summary Button -->
|
||||
<object name="summaryButton" type="button" style="StoneButton" size="65%-50 0 82%-50 100%">
|
||||
<translatableAttribute id="caption">Summary</translatableAttribute>
|
||||
|
@ -671,6 +671,11 @@ function leaveGame(willRejoin)
|
||||
|
||||
Engine.EndGame();
|
||||
|
||||
// After the replay file was closed in EndGame
|
||||
// Done here to keep EndGame small
|
||||
if (!g_IsReplay)
|
||||
Engine.AddReplayToCache(replayDirectory);
|
||||
|
||||
if (g_IsController && Engine.HasXmppClient())
|
||||
Engine.SendUnregisterGame();
|
||||
|
||||
|
@ -40,6 +40,9 @@
|
||||
*/
|
||||
const u8 minimumReplayDuration = 3;
|
||||
|
||||
static const OsPath tempCacheFileName = VisualReplay::GetDirectoryName() / L"replayCache_temp.json";
|
||||
static const OsPath cacheFileName = VisualReplay::GetDirectoryName() / L"replayCache.json";
|
||||
|
||||
OsPath VisualReplay::GetDirectoryName()
|
||||
{
|
||||
const Paths paths(g_args);
|
||||
@ -61,33 +64,171 @@ void VisualReplay::StartVisualReplay(const CStrW& directory)
|
||||
g_Game->StartVisualReplay(replayFile.string8());
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all replays found in the directory.
|
||||
*
|
||||
* Since files are spread across the harddisk,
|
||||
* loading hundreds of them can consume a lot of time.
|
||||
*/
|
||||
JS::Value VisualReplay::GetReplays(ScriptInterface& scriptInterface)
|
||||
bool VisualReplay::ReadCacheFile(ScriptInterface& scriptInterface, JS::MutableHandleObject cachedReplaysObject)
|
||||
{
|
||||
JSContext* cx = scriptInterface.GetContext();
|
||||
JSAutoRequest rq(cx);
|
||||
|
||||
if (!FileExists(cacheFileName))
|
||||
return false;
|
||||
|
||||
std::ifstream cacheStream(cacheFileName.string8().c_str());
|
||||
CStr cacheStr((std::istreambuf_iterator<char>(cacheStream)), std::istreambuf_iterator<char>());
|
||||
cacheStream.close();
|
||||
|
||||
JS::RootedValue cachedReplays(cx);
|
||||
if (scriptInterface.ParseJSON(cacheStr, &cachedReplays))
|
||||
{
|
||||
cachedReplaysObject.set(&cachedReplays.toObject());
|
||||
if (JS_IsArrayObject(cx, cachedReplaysObject))
|
||||
return true;
|
||||
}
|
||||
|
||||
LOGWARNING("The replay cache file is corrupted, it will be deleted");
|
||||
wunlink(cacheFileName);
|
||||
return false;
|
||||
}
|
||||
|
||||
void VisualReplay::StoreCacheFile(ScriptInterface& scriptInterface, JS::HandleObject replays)
|
||||
{
|
||||
JSContext* cx = scriptInterface.GetContext();
|
||||
JSAutoRequest rq(cx);
|
||||
|
||||
JS::RootedValue replaysRooted(cx, JS::ObjectValue(*replays));
|
||||
std::ofstream cacheStream(tempCacheFileName.string8().c_str(), std::ofstream::out | std::ofstream::trunc);
|
||||
cacheStream << scriptInterface.StringifyJSON(&replaysRooted);
|
||||
cacheStream.close();
|
||||
|
||||
wunlink(cacheFileName);
|
||||
if (wrename(tempCacheFileName, cacheFileName))
|
||||
LOGERROR("Could not store the replay cache");
|
||||
}
|
||||
|
||||
JS::HandleObject VisualReplay::ReloadReplayCache(ScriptInterface& scriptInterface, bool compareFiles)
|
||||
{
|
||||
TIMER(L"ReloadReplayCache");
|
||||
JSContext* cx = scriptInterface.GetContext();
|
||||
JSAutoRequest rq(cx);
|
||||
|
||||
// Maps the filename onto the index and size
|
||||
typedef std::map<CStr, std::pair<u32, off_t>> replayCacheMap;
|
||||
|
||||
replayCacheMap fileList;
|
||||
|
||||
JS::RootedObject cachedReplaysObject(cx);
|
||||
if (ReadCacheFile(scriptInterface, &cachedReplaysObject))
|
||||
{
|
||||
// Create list of files included in the cache
|
||||
u32 cacheLength = 0;
|
||||
JS_GetArrayLength(cx, cachedReplaysObject, &cacheLength);
|
||||
for (u32 j = 0; j < cacheLength; ++j)
|
||||
{
|
||||
JS::RootedValue replay(cx);
|
||||
JS_GetElement(cx, cachedReplaysObject, j, &replay);
|
||||
|
||||
JS::RootedValue file(cx);
|
||||
CStr fileName;
|
||||
double fileSize;
|
||||
scriptInterface.GetProperty(replay, "directory", fileName);
|
||||
scriptInterface.GetProperty(replay, "fileSize", fileSize);
|
||||
|
||||
fileList[fileName] = std::make_pair(j, fileSize);
|
||||
}
|
||||
}
|
||||
|
||||
JS::RootedObject replays(cx, JS_NewArrayObject(cx, 0));
|
||||
DirectoryNames directories;
|
||||
|
||||
if (GetDirectoryEntries(GetDirectoryName(), nullptr, &directories) != INFO::OK)
|
||||
return replays;
|
||||
|
||||
bool newReplays = false;
|
||||
std::vector<u32> copyFromOldCache;
|
||||
// Specifies where the next replay should be kept
|
||||
u32 i = 0;
|
||||
|
||||
for (const OsPath& directory : directories)
|
||||
{
|
||||
if (SDL_QuitRequested())
|
||||
// We want to save our progress in searching through the replays
|
||||
break;
|
||||
|
||||
bool isNew = true;
|
||||
replayCacheMap::iterator it = fileList.find(directory.string8());
|
||||
if (it != fileList.end())
|
||||
{
|
||||
if (compareFiles)
|
||||
{
|
||||
CFileInfo fileInfo;
|
||||
GetFileInfo(GetDirectoryName() / directory / L"commands.txt", &fileInfo);
|
||||
if (fileInfo.Size() == it->second.second)
|
||||
isNew = false;
|
||||
}
|
||||
else
|
||||
isNew = false;
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
JS::RootedValue replayData(cx, LoadReplayData(scriptInterface, directory));
|
||||
if (replayData.isNull())
|
||||
{
|
||||
CFileInfo fileInfo;
|
||||
GetFileInfo(GetDirectoryName() / directory / L"commands.txt", &fileInfo);
|
||||
scriptInterface.Eval("({})", &replayData);
|
||||
scriptInterface.SetProperty(replayData, "directory", directory);
|
||||
scriptInterface.SetProperty(replayData, "fileSize", (double)fileInfo.Size());
|
||||
}
|
||||
JS_SetElement(cx, replays, i++, replayData);
|
||||
newReplays = true;
|
||||
}
|
||||
else
|
||||
copyFromOldCache.push_back(it->second.first);
|
||||
}
|
||||
|
||||
debug_printf(
|
||||
"Loading %lu cached replays, removed %lu outdated entries, loaded %i new entries\n",
|
||||
(unsigned long)fileList.size(), (unsigned long)(fileList.size() - copyFromOldCache.size()), i);
|
||||
|
||||
if (!newReplays && fileList.empty())
|
||||
return replays;
|
||||
|
||||
// No replay was changed, so just return the cache
|
||||
if (!newReplays && fileList.size() == copyFromOldCache.size())
|
||||
return cachedReplaysObject;
|
||||
|
||||
{
|
||||
// Copy the replays from the old cache that are not deleted
|
||||
if (!copyFromOldCache.empty())
|
||||
for (u32 j : copyFromOldCache)
|
||||
{
|
||||
JS::RootedValue replay(cx);
|
||||
JS_GetElement(cx, cachedReplaysObject, j, &replay);
|
||||
JS_SetElement(cx, replays, i++, replay);
|
||||
}
|
||||
}
|
||||
StoreCacheFile(scriptInterface, replays);
|
||||
return replays;
|
||||
}
|
||||
|
||||
JS::Value VisualReplay::GetReplays(ScriptInterface& scriptInterface, bool compareFiles)
|
||||
{
|
||||
TIMER(L"GetReplays");
|
||||
JSContext* cx = scriptInterface.GetContext();
|
||||
JSAutoRequest rq(cx);
|
||||
|
||||
u32 i = 0;
|
||||
DirectoryNames directories;
|
||||
JS::RootedObject replays(cx, JS_NewArrayObject(cx, 0));
|
||||
|
||||
if (GetDirectoryEntries(GetDirectoryName(), NULL, &directories) == INFO::OK)
|
||||
for (OsPath& directory : directories)
|
||||
{
|
||||
if (SDL_QuitRequested())
|
||||
return JSVAL_NULL;
|
||||
|
||||
JS::RootedValue replayData(cx, LoadReplayData(scriptInterface, directory));
|
||||
if (!replayData.isNull())
|
||||
JS_SetElement(cx, replays, i++, replayData);
|
||||
}
|
||||
return JS::ObjectValue(*replays);
|
||||
JS::RootedObject replays(cx, ReloadReplayCache(scriptInterface, compareFiles));
|
||||
// Only take entries with data
|
||||
JS::RootedObject replaysWithoutNullEntries(cx, JS_NewArrayObject(cx, 0));
|
||||
u32 replaysLength = 0;
|
||||
JS_GetArrayLength(cx, replays, &replaysLength);
|
||||
for (u32 j = 0, i = 0; j < replaysLength; ++j)
|
||||
{
|
||||
JS::RootedValue replay(cx);
|
||||
JS_GetElement(cx, replays, j, &replay);
|
||||
if (scriptInterface.HasProperty(replay, "attribs"))
|
||||
JS_SetElement(cx, replaysWithoutNullEntries, i++, replay);
|
||||
}
|
||||
return JS::ObjectValue(*replaysWithoutNullEntries);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -173,7 +314,7 @@ inline int getReplayDuration(std::istream* replayStream, const CStr& fileName, c
|
||||
return -1;
|
||||
}
|
||||
|
||||
JS::Value VisualReplay::LoadReplayData(ScriptInterface& scriptInterface, OsPath& directory)
|
||||
JS::Value VisualReplay::LoadReplayData(ScriptInterface& scriptInterface, const OsPath& directory)
|
||||
{
|
||||
// The directory argument must not be constant, otherwise concatenating will fail
|
||||
const OsPath replayFile = GetDirectoryName() / directory / L"commands.txt";
|
||||
@ -250,6 +391,7 @@ JS::Value VisualReplay::LoadReplayData(ScriptInterface& scriptInterface, OsPath&
|
||||
scriptInterface.Eval("({})", &replayData);
|
||||
scriptInterface.SetProperty(replayData, "file", replayFile);
|
||||
scriptInterface.SetProperty(replayData, "directory", directory);
|
||||
scriptInterface.SetProperty(replayData, "fileSize", (double)fileSize);
|
||||
scriptInterface.SetProperty(replayData, "attribs", attribs);
|
||||
scriptInterface.SetProperty(replayData, "duration", duration);
|
||||
return replayData;
|
||||
@ -264,7 +406,6 @@ bool VisualReplay::DeleteReplay(const CStrW& replayDirectory)
|
||||
return DirectoryExists(directory) && DeleteDirectory(directory) == INFO::OK;
|
||||
}
|
||||
|
||||
|
||||
JS::Value VisualReplay::GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName)
|
||||
{
|
||||
// Create empty JS object
|
||||
@ -290,6 +431,27 @@ JS::Value VisualReplay::GetReplayAttributes(ScriptInterface::CxPrivate* pCxPriva
|
||||
return attribs;
|
||||
}
|
||||
|
||||
void VisualReplay::AddReplayToCache(ScriptInterface& scriptInterface, const CStrW& directoryName)
|
||||
{
|
||||
TIMER(L"AddReplayToCache");
|
||||
JSContext* cx = scriptInterface.GetContext();
|
||||
JSAutoRequest rq(cx);
|
||||
|
||||
JS::RootedValue replayData(cx, LoadReplayData(scriptInterface, OsPath(directoryName)));
|
||||
if (replayData.isNull())
|
||||
return;
|
||||
|
||||
JS::RootedObject cachedReplaysObject(cx);
|
||||
if (!ReadCacheFile(scriptInterface, &cachedReplaysObject))
|
||||
cachedReplaysObject = JS_NewArrayObject(cx, 0);
|
||||
|
||||
u32 cacheLength = 0;
|
||||
JS_GetArrayLength(cx, cachedReplaysObject, &cacheLength);
|
||||
JS_SetElement(cx, cachedReplaysObject, cacheLength, replayData);
|
||||
|
||||
StoreCacheFile(scriptInterface, cachedReplaysObject);
|
||||
}
|
||||
|
||||
void VisualReplay::SaveReplayMetadata(ScriptInterface* scriptInterface)
|
||||
{
|
||||
JSContext* cx = scriptInterface->GetContext();
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2016 Wildfire Games.
|
||||
/* Copyright (C) 2017 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
@ -31,7 +31,7 @@ namespace VisualReplay
|
||||
/**
|
||||
* Returns the path to the sim-log directory (that contains the directories with the replay files.
|
||||
*
|
||||
* @param scriptInterface the ScriptInterface in which to create the return data.
|
||||
* @param scriptInterface - the ScriptInterface in which to create the return data.
|
||||
* @return OsPath the absolute file path
|
||||
*/
|
||||
OsPath GetDirectoryName();
|
||||
@ -41,24 +41,52 @@ OsPath GetDirectoryName();
|
||||
*/
|
||||
void StartVisualReplay(const CStrW& directory);
|
||||
|
||||
/**
|
||||
* Reads the replay Cache file and parses it into a jsObject
|
||||
*
|
||||
* @param scriptInterface - the ScriptInterface in which to create the return data.
|
||||
* @param cachedReplaysObject - the cached replays.
|
||||
* @return true on succes
|
||||
*/
|
||||
bool ReadCacheFile(ScriptInterface& scriptInterface, JS::MutableHandleObject cachedReplaysObject);
|
||||
|
||||
/**
|
||||
* Stores the replay list in the replay cache file
|
||||
*
|
||||
* @param scriptInterface - the ScriptInterface in which to create the return data.
|
||||
* @param replays - the replay list to store.
|
||||
*/
|
||||
void StoreCacheFile(ScriptInterface& scriptInterface, JS::HandleObject replays);
|
||||
|
||||
/**
|
||||
* Load the replay cache and check if there are new/deleted replays. If so, update the cache.
|
||||
*
|
||||
* @param scriptInterface - the ScriptInterface in which to create the return data.
|
||||
* @param compareFiles - compare the directory name and the FileSize of the replays and the cache.
|
||||
* @return cache entries
|
||||
*/
|
||||
JS::HandleObject ReloadReplayCache(ScriptInterface& scriptInterface, bool compareFiles);
|
||||
|
||||
/**
|
||||
* Get a list of replays to display in the GUI.
|
||||
*
|
||||
* @param scriptInterface the ScriptInterface in which to create the return data.
|
||||
* @param scriptInterface - the ScriptInterface in which to create the return data.
|
||||
* @param compareFiles - reload the cache, which takes more time,
|
||||
* but nearly ensures, that no changed replay is missed.
|
||||
* @return array of objects containing replay data
|
||||
*/
|
||||
JS::Value GetReplays(ScriptInterface& scriptInterface);
|
||||
JS::Value GetReplays(ScriptInterface& scriptInterface, bool compareFiles);
|
||||
|
||||
/**
|
||||
* Parses a commands.txt file and extracts metadata.
|
||||
* Works similarly to CGame::LoadReplayData().
|
||||
*/
|
||||
JS::Value LoadReplayData(ScriptInterface& scriptInterface, OsPath& directory);
|
||||
JS::Value LoadReplayData(ScriptInterface& scriptInterface, const OsPath& directory);
|
||||
|
||||
/**
|
||||
* Permanently deletes the visual replay (including the parent directory)
|
||||
*
|
||||
* @param replayFile path to commands.txt, whose parent directory will be deleted
|
||||
* @param replayFile - path to commands.txt, whose parent directory will be deleted.
|
||||
* @return true if deletion was successful, false on error
|
||||
*/
|
||||
bool DeleteReplay(const CStrW& replayFile);
|
||||
@ -79,10 +107,14 @@ bool HasReplayMetadata(const CStrW& directoryName);
|
||||
JS::Value GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName);
|
||||
|
||||
/**
|
||||
* Saves the metadata from the session to metadata.json
|
||||
* Saves the metadata from the session to metadata.json.
|
||||
*/
|
||||
void SaveReplayMetadata(ScriptInterface* scriptInterface);
|
||||
|
||||
/**
|
||||
* Adds a replay to the replayCache.
|
||||
*/
|
||||
void AddReplayToCache(ScriptInterface& scriptInterface, const CStrW& directoryName);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
@ -33,9 +33,9 @@ bool JSI_VisualReplay::DeleteReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivat
|
||||
return VisualReplay::DeleteReplay(replayFile);
|
||||
}
|
||||
|
||||
JS::Value JSI_VisualReplay::GetReplays(ScriptInterface::CxPrivate* pCxPrivate)
|
||||
JS::Value JSI_VisualReplay::GetReplays(ScriptInterface::CxPrivate* pCxPrivate, bool compareFiles)
|
||||
{
|
||||
return VisualReplay::GetReplays(*(pCxPrivate->pScriptInterface));
|
||||
return VisualReplay::GetReplays(*(pCxPrivate->pScriptInterface), compareFiles);
|
||||
}
|
||||
|
||||
JS::Value JSI_VisualReplay::GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName)
|
||||
@ -53,6 +53,11 @@ JS::Value JSI_VisualReplay::GetReplayMetadata(ScriptInterface::CxPrivate* pCxPri
|
||||
return VisualReplay::GetReplayMetadata(pCxPrivate, directoryName);
|
||||
}
|
||||
|
||||
void JSI_VisualReplay::AddReplayToCache(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName)
|
||||
{
|
||||
VisualReplay::AddReplayToCache(*(pCxPrivate->pScriptInterface), directoryName);
|
||||
}
|
||||
|
||||
CStrW JSI_VisualReplay::GetReplayDirectoryName(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const CStrW& directoryName)
|
||||
{
|
||||
return OsPath(VisualReplay::GetDirectoryName() / directoryName).string();
|
||||
@ -60,11 +65,12 @@ CStrW JSI_VisualReplay::GetReplayDirectoryName(ScriptInterface::CxPrivate* UNUSE
|
||||
|
||||
void JSI_VisualReplay::RegisterScriptFunctions(ScriptInterface& scriptInterface)
|
||||
{
|
||||
scriptInterface.RegisterFunction<JS::Value, &GetReplays>("GetReplays");
|
||||
scriptInterface.RegisterFunction<JS::Value, bool, &GetReplays>("GetReplays");
|
||||
scriptInterface.RegisterFunction<bool, CStrW, &DeleteReplay>("DeleteReplay");
|
||||
scriptInterface.RegisterFunction<void, CStrW, &StartVisualReplay>("StartVisualReplay");
|
||||
scriptInterface.RegisterFunction<JS::Value, CStrW, &GetReplayAttributes>("GetReplayAttributes");
|
||||
scriptInterface.RegisterFunction<JS::Value, CStrW, &GetReplayMetadata>("GetReplayMetadata");
|
||||
scriptInterface.RegisterFunction<bool, CStrW, &HasReplayMetadata>("HasReplayMetadata");
|
||||
scriptInterface.RegisterFunction<void, CStrW, &AddReplayToCache>("AddReplayToCache");
|
||||
scriptInterface.RegisterFunction<CStrW, CStrW, &GetReplayDirectoryName>("GetReplayDirectoryName");
|
||||
}
|
||||
|
@ -25,10 +25,11 @@ namespace JSI_VisualReplay
|
||||
{
|
||||
void StartVisualReplay(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directory);
|
||||
bool DeleteReplay(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& replayFile);
|
||||
JS::Value GetReplays(ScriptInterface::CxPrivate* pCxPrivate);
|
||||
JS::Value GetReplays(ScriptInterface::CxPrivate* pCxPrivate, bool compareFiles);
|
||||
JS::Value GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName);
|
||||
bool HasReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName);
|
||||
JS::Value GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName);
|
||||
void AddReplayToCache(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName);
|
||||
void RegisterScriptFunctions(ScriptInterface& scriptInterface);
|
||||
CStrW GetReplayDirectoryName(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user