Skip to content

Commit

Permalink
[Experimental] Asynchronous folder read
Browse files Browse the repository at this point in the history
* Fix broken 'jump to another folder'
  * Thus, child/parallel folders do NOT benefit from async folder read atm
  • Loading branch information
sdneon committed Jun 11, 2023
1 parent 628406a commit e79ff11
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 17 deletions.
30 changes: 25 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ JPEGView is a lean, fast and highly configurable image viewer/editor with a mini
JPEGView has built-in support the following formats:

* Popular: JPEG, GIF
* Lossless: BMP, PNG, TIFF, QOI, ICO
* Lossless: BMP, PNG, TIFF, QOI, ICO (mod only)
* Web: WEBP, JXL, HEIF/HEIC, AVIF (common subset)
* Legacy: TGA, WDP, HDP, JXR
* Camera RAW formats:
Expand Down Expand Up @@ -47,7 +47,7 @@ Basic on-the-fly image processing is provided - allowing adjusting typical param
* Default to panning mode. Dedicated 'Selection mode' can be toggled via remapped 'S' hotkey.
* Quick zoom to selection mode via remapped hotkey 'Z'.
* Option for selection box to match image aspect ratio.
* Toggle transparent image background between checkerboard pattern (default) and solid background colour, via hotkey: SHIFT+V.
* Toggle 3 transparency modes: TransparencyColor (default), checkerboard pattern or inverse TransparencyColor, via hotkey: SHIFT+V.
* Navigation
* **ALT+<Left/Right arrow>**: Jump back/forward 100 images.
* **CTRL+ALT+<Left/Right arrow>**: Jump to previous/next folder.
Expand All @@ -60,10 +60,14 @@ Basic on-the-fly image processing is provided - allowing adjusting typical param
* Old format still supported.
* Use ConvertKeyMap tool to make one-off conversion if desired.
* Others
* Release includes all necessary DLLs.
* Toast notifications.
* Toggle ascending/descending sorting by pressing the same hotkey for sorting mode.
* Added `Auto` folder navigation mode to auto-choose `LoopSubFolders` (if initial folder has subfolder) or `LoopSameFolderLevel` (otherwise).
* Command# 6000 (LOOP_FOLDER, hotkey: F7) now toggles between `LoopFolder` and `Auto`.
* New settings and/or options:
* Added `Auto` folder navigation mode to auto-choose `LoopSubFolders` (if initial folder has subfolder) or `LoopSameFolderLevel` (otherwise).
* Command# 6000 (LOOP_FOLDER, hotkey: F7) now toggles between `LoopFolder` and `Auto`.
* Set `MinFilesize > 0` to Hide of small images. It auto-disables temporarily if 1st image opened is small (< MinFilesize), so as to view that image as intended.
* [Experimental]: include a mod of [mez0ru's PR for quick image show despite large folder](https://github.com/sylikc/jpegview/pull/172)

(Last selectively sync'd up to original's ~31 Jan 2023 updates, with occasional cherry picks going ahead).

Expand Down Expand Up @@ -94,7 +98,8 @@ JPEGView has a slideshow mode which can be activated in various ways:
* E.g.: `JPEGView.exe /slideshow 2`
(**modified in mod**) starts JPEGView in image selection mode, and then starts slideshow with image switching at 2s intervals. (Previously when an image/path is not specified, `/slideshow` is ignored)
* Slideshow no longer paused when jumping into an image from a different folder.
* `FolderNavigation` setting defaults to `Auto`.
* New `Auto` option for `FolderNavigation` setting.
* During slideshow, block screensaver.
* A little Android-like `toast` to inform of new slideshow fps or interval. Also used for other general notifications of interest.
* PS: there's an existing toast-like display of zoom factor when zooming - just in a smaller font.

Expand Down Expand Up @@ -178,6 +183,21 @@ Configure in `JPEGView.ini`:
* Specify in bytes, KB or MB like so: 30720, 30K or 1M
* Enable hide hidden images and folders: `HideHidden`, default: true.

### [Experimental] Asynchronous FileList

mez0ru has a [PR](https://github.com/sylikc/jpegview/pull/172) for [Issue: Slow startup when opening a file in a highly populated folder](https://github.com/sylikc/jpegview/issues/194).

I've tried it out a _mod_ of it on a folder with 25k small images. Differences:
* If app is launched with an image specified (instead of a folder), it will show that image (by adding it to the file list) first. Asynchronous loading of files in that image's folder, is started thereafter. This ensures that initial image is quickly displayed.
* Limit code changes to FileList.cpp/h files only.
* NextFolder() does NOT play well with async load, as underlying FindFileRecursively() & TryCreateFileList() will mistakenly discard all folders thinking they are empty, and stay in original folder. So we've to force a full load, and lose the benefit of async-load of child/parallel folders.
This needs more work!
* Perhaps rework FindFiles() into 2 parts: (1) return 1st file found (to let FindFileRecursively() succeed in retaining non-empty folders) and (2) return rest of files.

From timing printouts, it does help 'significantly'.
However 'by feel', perhaps owing to my defragmented drive, opening an image in a folder with 25k is still _very fast_ (< 1s)! Thus, I can't really test other situations like jumping 100 images, etc. As such, I'll leave this patch as is, for now.


### Wishlist

* Filter images by date, like show newest images only?
Expand Down
62 changes: 51 additions & 11 deletions src/JPEGView/FileList.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,9 @@ CFileList::CFileList(const CString & sInitialFile, CDirectoryWatcher & directory
int nMinFilesize, bool bHideHidden)
: m_directoryWatcher(directoryWatcher),
m_nMinFilesize(nMinFilesize),
m_bHideHidden(bHideHidden)
m_bHideHidden(bHideHidden),
m_bFindingFiles(false),
m_bForceExitFindFiles(false)
{

CFileDesc::SetSorting(eInitialSorting, isSortedUpcounting);
Expand Down Expand Up @@ -226,9 +228,30 @@ CFileList::CFileList(const CString & sInitialFile, CDirectoryWatcher & directory

if (!m_bIsSlideShowList) {
if (bImageFile || bIsDirectory) {
FindFiles();
m_iter = FindFile(sInitialFile);
m_iterStart = m_fileList.begin();
m_iterStart = m_iter = m_fileList.begin();
if (bImageFile)
{ //at least show 1st selected image 1st
CFindFile fileFind;
if (fileFind.FindFile(sInitialFile)) {
AddToFileList(m_fileList, fileFind, sExtensionInitialFile, m_nMinFilesize, m_bHideHidden);
m_iterStart = m_iter = m_fileList.begin();
}
else
bImageFile = false;
}
//move potentially heavy search to separate thread
m_bFindingFiles = true;
m_taskFindFiles = std::async(std::launch::async, [this, &sInitialFile, &bWrapAroundFolder, &bImageFile]() {
FindFiles(!bImageFile);
if (m_bForceExitFindFiles) {
m_bForceExitFindFiles = false;
return;
}
m_iter = FindFile(sInitialFile);
m_iterStart = m_fileList.begin();
m_bFindingFiles = false;
m_bForceExitFindFiles = false;
});
} else {
// neither image file nor directory nor list of file names - try to read anyway but normally will fail
CFindFile fileFind;
Expand Down Expand Up @@ -265,6 +288,11 @@ CString CFileList::GetSupportedFileEndings() {
void CFileList::Reload(LPCTSTR sFileName, bool clearForwardHistory) {
LPCTSTR sCurrent = sFileName;
if (sCurrent == NULL) {
// If async is still processing, quickly terminate it.
m_bForceExitFindFiles = true;
WaitIfNotReady();
m_bForceExitFindFiles = false;

sCurrent = Current();
if (sCurrent == NULL) {
m_fileList.clear();
Expand Down Expand Up @@ -347,7 +375,7 @@ void CFileList::FileHasRenamed(LPCTSTR sOldFileName, LPCTSTR sNewFileName) {
m_sInitialFile = sNewFileName;
}
std::list<CFileDesc>::iterator iter;
for (iter = m_fileList.begin( ); iter != m_fileList.end( ); iter++ ) {
for (iter = m_fileList.begin(); iter != m_fileList.end(); iter++ ) {
if (_tcsicmp(sOldFileName, iter->GetName()) == 0) {
iter->SetName(sNewFileName);
}
Expand Down Expand Up @@ -497,6 +525,8 @@ void CFileList::Last() {
}

CFileList* CFileList::AwayFromCurrent() {
WaitIfNotReady(); // If async is still processing, then wait.

LPCTSTR sCurrentFile = Current();
LPCTSTR sNextFile = PeekNextPrev(1, true, false);
if (sCurrentFile == NULL || sNextFile == NULL || _tcscmp(sCurrentFile, sNextFile) == 0) {
Expand All @@ -512,6 +542,8 @@ CFileList* CFileList::AwayFromCurrent() {
}

LPCTSTR CFileList::Current() const {
if ((m_fileList.size() == 0) && m_bFindingFiles)
m_taskFindFiles.wait();
if (m_iter != m_fileList.end()) {
return m_iter->GetName();
} else {
Expand Down Expand Up @@ -541,7 +573,7 @@ LPCTSTR CFileList::CurrentDirectory() const {
int CFileList::CurrentIndex() const {
int i = 0;
std::list<CFileDesc>::const_iterator iter;
for (iter = m_fileList.begin( ); iter != m_fileList.end( ); iter++ ) {
for (iter = m_fileList.begin(); iter != m_fileList.end(); iter++) {
if (iter == m_iter) {
return i;
}
Expand Down Expand Up @@ -696,7 +728,7 @@ std::list<CFileDesc>::iterator CFileList::FindFile(const CString& sName) {
return m_fileList.begin();
}
std::list<CFileDesc>::iterator iter;
for (iter = m_fileList.begin( ); iter != m_fileList.end( ); iter++ ) {
for (iter = m_fileList.begin(); iter != m_fileList.end(); iter++ ) {
if (_tcsicmp((LPCTSTR)sName + nStart, iter->GetTitle()) == 0) {
return iter;
}
Expand Down Expand Up @@ -792,7 +824,7 @@ CFileList* CFileList::WrapToNextImage() {
std::list<CString>::iterator iter;
bool bFound = false;
for (int nStep = 0; nStep < 2; nStep++) {
for (iter = dirList.begin( ); iter != dirList.end( ); iter++ ) {
for (iter = dirList.begin(); iter != dirList.end(); iter++ ) {
if (iter->CompareNoCase(sThisDirTitle) == 0) {
bFound = true;
} else if (bFound) {
Expand Down Expand Up @@ -976,6 +1008,12 @@ CFileList* CFileList::TryCreateFileList(const CString& directory, int nNewLevel,
}

CFileList* pNewList = new CFileList(directory, m_directoryWatcher, CFileDesc::GetSorting(), CFileDesc::IsSortedUpcounting(), m_bWrapAroundFolder, nNewLevel, m_bHideHidden);
/*
* Async load folder will return 0 items initially and cause NextFolder() to fail!
* So force WaitIfNotReady.
* Would shortcut return upon finding 1 file be sufficient?
*/
pNewList->WaitIfNotReady();
if (pNewList->m_fileList.size() > 0) {
if (!bInverse)
{
Expand All @@ -994,15 +1032,17 @@ CFileList* CFileList::TryCreateFileList(const CString& directory, int nNewLevel,
}
}

void CFileList::FindFiles() {
m_fileList.clear();
void CFileList::FindFiles(bool bPurge1st) {
if (bPurge1st) m_fileList.clear();
if (!m_sDirectory.IsEmpty()) {
CFindFile fileFind;
LPCTSTR* allFileEndings = GetSupportedFileEndingList();
for (int i = 0; i < nNumEndings; i++) {
if (m_bForceExitFindFiles) return;
if (fileFind.FindFile(m_sDirectory + _T("\\*.") + allFileEndings[i])) {
AddToFileList(m_fileList, fileFind, allFileEndings[i], m_nMinFilesize, m_bHideHidden);
while (fileFind.FindNextFile()) {
if (m_bForceExitFindFiles) return;
AddToFileList(m_fileList, fileFind, allFileEndings[i], m_nMinFilesize, m_bHideHidden);
}
}
Expand All @@ -1014,7 +1054,7 @@ void CFileList::FindFiles() {

void CFileList::VerifyFiles() {
std::list<CFileDesc>::iterator iter;
for (iter = m_fileList.begin( ); iter != m_fileList.end( ); iter++ ) {
for (iter = m_fileList.begin(); iter != m_fileList.end(); iter++ ) {
if (::GetFileAttributes(iter->GetName()) == INVALID_FILE_ATTRIBUTES) {
iter = m_fileList.erase(iter);
if (iter == m_fileList.end()) {
Expand Down
9 changes: 8 additions & 1 deletion src/JPEGView/FileList.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include "Helpers.h"
#include <future>

class CDirectoryWatcher;

Expand Down Expand Up @@ -173,6 +174,8 @@ class CFileList
int m_nMarkedIndexShow;

CDirectoryWatcher & m_directoryWatcher;
std::future<void> m_taskFindFiles;
bool m_bFindingFiles, m_bForceExitFindFiles;

void MoveIterToLast();
void NextInFolder();
Expand All @@ -187,8 +190,12 @@ class CFileList
bool HasSubDir();
void GetDirListRcursive(CString sPath, std::list<CString> &dirList, CString &sThisDirTitle);
CFileList* AnyAvailableLast();
void FindFiles();
void FindFiles(bool bPurge1st = true);
void VerifyFiles();
bool IsImageFile(const CString & sEnding);
bool TryReadingSlideShowList(const CString & sSlideShowFile);
inline void CFileList::WaitIfNotReady() {
if (m_bFindingFiles)
m_taskFindFiles.wait();
}
};

0 comments on commit e79ff11

Please sign in to comment.