1
1
forked from 0ad/0ad

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:
Imarok 2017-05-27 20:17:57 +00:00
parent d096c2f09c
commit 80635665f7
8 changed files with 261 additions and 39 deletions

View File

@ -127,6 +127,12 @@ function showReplaySummary()
});
}
function reloadCache()
{
let selected = Engine.GetGUIObjectByName("replaySelection").selected;
loadReplays(selected > -1 ? createReplaySelectionData(g_ReplaysFiltered[selected].directory) : "", true);
}
/**
* Callback.
*/

View File

@ -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;

View File

@ -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>

View File

@ -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();

View File

@ -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();

View File

@ -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

View File

@ -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");
}

View File

@ -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);
}