Skip to content

Commit 75440e0

Browse files
committed
Update way in which column data is retrieved and set
Previously, column data was retrieved on a background thread. That thread would iterate through each item in the listview, retrieve the column data then insert it into the listview. Now, the column data is still retrieved in a background thread, but items are only queued to the thread as necessary (e.g. as items are scrolled into view). Additionally, the background thread no longer interacts with the listview at all. The updated implementation has a number of advantages: - As mentioned above, items are only queued when necessary. This means that not all the results have to be loaded at once. - Because the background thread no longer touches the UI, it's potentially easier to lock its access to shared data. Previously, acquiring a lock in the background thread and then attempting to lock the main thread could result in a deadlock. The background thread would try to update the UI, which would require work to be done on the main thread, but the main thread would be locked waiting on the background thread. Note that this locking wasn't actually added. It wouldn't have been safe to do so, for the above reason. - Since the background thread no longer touches the UI, it may help to cut down the crashes that can occur when navigating to a different folder while column results are being filled. Specifically, the first thing the previous background thread did for an item was to look that item up in the listview. That could fail (if a navigation had occurred since the item was queued), potentially causing a crash. - Because the previous implementation updated the UI directly, it was possible for a UI update to be done after a navigation had occurred. That is, column data for an item in a previous folder would be inserted into the listview for the current folder. In the new implementation, this isn't possible. Results are posted back to the main thread and the main thread will ignore results for previous folders. - Both the listview item index and column index are looked up at the point where the column data is set (in the main thread). Previously, the item index and column index were determined before the column text was retrieved. This meant it was possible for the indexes to change before the text had been set. - Parts of the implementation are simplified, since a library is used for the thread pool functionality (rather than custom code). - It's trivial to increase the number of threads used in the thread pool (though that may not be of too much use in this specific case, as the work is IO bound). However, there is still at least one remaining issue. The background thread shares state with the main thread (the same object is used for both). At the moment, there's no locking that's done. That means that if a navigation occurs while column data is being retrieved, or a file is modified or removed, the application could still crash. Adding locks to the appropriate code may be easier now that the background thread doesn't touch the UI, but ultimately, there's still too much state that's being shared. Another potential issue with the new implementation is that the library being used for the thread pool functionality (https://github.com/vit-vit/CTPL) hasn't been updated since 2015. It may not receive any future bug fixes. The library is fairly simple though and could potentially be replaced if necessary. Any replacement would need the following functionality: - The ability to control the number of threads in the pool. - The ability to push tasks to a queue. - The ability to clear the queue. - The ability to retrieve results using std::future.
1 parent 1737384 commit 75440e0

File tree

9 files changed

+456
-101
lines changed

9 files changed

+456
-101
lines changed

Explorer++/Explorer++/Explorer++.vcxproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@
346346
<ClCompile Include="ShellBrowser\iFolderView.cpp" />
347347
<ClCompile Include="ShellBrowser\iPathManager.cpp" />
348348
<ClCompile Include="ShellBrowser\iShellBrowser.cpp" />
349+
<ClCompile Include="ShellBrowser\ListView.cpp" />
349350
<ClCompile Include="ShellBrowser\SortManager.cpp" />
350351
<ClCompile Include="ShellContextMenuHandler.cpp" />
351352
<ClCompile Include="SplitFileDialog.cpp" />

Explorer++/Explorer++/Explorer++.vcxproj.filters

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,9 @@
232232
<ClCompile Include="ShellBrowser\SortManager.cpp">
233233
<Filter>ShellBrowser</Filter>
234234
</ClCompile>
235+
<ClCompile Include="ShellBrowser\ListView.cpp">
236+
<Filter>ShellBrowser</Filter>
237+
</ClCompile>
235238
</ItemGroup>
236239
<ItemGroup>
237240
<ClInclude Include="ApplicationToolbar.h">

Explorer++/Explorer++/ShellBrowser/BrowsingHandler.cpp

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,13 @@ HRESULT CShellBrowser::BrowseFolder(LPCITEMIDLIST pidlDirectory,UINT wFlags)
6363

6464
EmptyIconFinderQueue();
6565
EmptyThumbnailsQueue();
66-
EmptyColumnQueue();
6766
EmptyFolderQueue();
6867

6968
/* TODO: Wait for any background threads to finish processing. */
7069

70+
m_columnThreadPool.clear_queue();
71+
m_columnResults.clear();
72+
7173
EnterCriticalSection(&m_csDirectoryAltered);
7274
m_FilesAdded.clear();
7375
m_FileSelectionList.clear();
@@ -268,8 +270,6 @@ void inline CShellBrowser::InsertAwaitingItems(BOOL bInsertIntoGroup)
268270
TCHAR szDrive[MAX_PATH];
269271
BOOL bNetworkRemovable = FALSE;
270272

271-
QueueUserAPC(SetAllColumnDataAPC,m_hThread,(ULONG_PTR)this);
272-
273273
StringCchCopy(szDrive,SIZEOF_ARRAY(szDrive),m_CurDir);
274274
PathStripToRoot(szDrive);
275275

@@ -567,7 +567,6 @@ HRESULT inline CShellBrowser::AddItemInternal(int iItemIndex,int iItemId,BOOL bP
567567

568568
m_AwaitingAddList.push_back(AwaitingAdd);
569569

570-
AddToColumnQueue(AwaitingAdd.iItem);
571570
AddToFolderQueue(AwaitingAdd.iItem);
572571

573572
return S_OK;

Explorer++/Explorer++/ShellBrowser/ColumnManager.cpp

Lines changed: 104 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -35,63 +35,6 @@
3535

3636
BOOL GetPrinterStatusDescription(DWORD dwStatus, TCHAR *szStatus, size_t cchMax);
3737

38-
/* Queueing model:
39-
When first browsing into a folder, all items in queue
40-
are cleared.
41-
Then, all new items are added.
42-
Finally, the APC is queued to the worker thread.
43-
44-
The worker thread will process items, removing items
45-
from the queue as it does. Interlocking is required
46-
between queue removal and emptying the queue.
47-
48-
Folder sizes are NOT calculated here. They are done
49-
within a separate thread called from the main thread. */
50-
void CShellBrowser::AddToColumnQueue(int iItem)
51-
{
52-
m_pColumnInfoList.push_back(iItem);
53-
}
54-
55-
void CShellBrowser::EmptyColumnQueue(void)
56-
{
57-
EnterCriticalSection(&m_column_cs);
58-
59-
m_pColumnInfoList.clear();
60-
61-
LeaveCriticalSection(&m_column_cs);
62-
}
63-
64-
BOOL CShellBrowser::RemoveFromColumnQueue(int *iItem)
65-
{
66-
BOOL bQueueNotEmpty;
67-
68-
EnterCriticalSection(&m_column_cs);
69-
70-
if(m_pColumnInfoList.empty() == TRUE)
71-
{
72-
SetEvent(m_hColumnQueueEvent);
73-
bQueueNotEmpty = FALSE;
74-
}
75-
else
76-
{
77-
std::list<int>::iterator itr;
78-
79-
itr = m_pColumnInfoList.begin();
80-
81-
*iItem = *itr;
82-
83-
ResetEvent(m_hColumnQueueEvent);
84-
85-
m_pColumnInfoList.erase(itr);
86-
87-
bQueueNotEmpty = TRUE;
88-
}
89-
90-
LeaveCriticalSection(&m_column_cs);
91-
92-
return bQueueNotEmpty;
93-
}
94-
9538
void CShellBrowser::AddToFolderQueue(int iItem)
9639
{
9740
m_pFolderInfoList.push_back(iItem);
@@ -223,33 +166,122 @@ int CShellBrowser::SetAllFolderSizeColumnData(void)
223166
return 0;
224167
}
225168

226-
void CALLBACK SetAllColumnDataAPC(ULONG_PTR dwParam)
169+
void CShellBrowser::QueueColumnTask(int itemInternalIndex, int columnIndex)
227170
{
228-
CShellBrowser *pShellBrowser = reinterpret_cast<CShellBrowser *>(dwParam);
229-
pShellBrowser->SetAllColumnText();
171+
auto columnID = GetColumnIdByIndex(columnIndex);
172+
173+
if (!columnID)
174+
{
175+
return;
176+
}
177+
178+
int columnResultID = m_columnResultIDCounter++;
179+
180+
auto result = m_columnThreadPool.push([this, columnResultID, columnID, itemInternalIndex](int id) {
181+
UNREFERENCED_PARAMETER(id);
182+
183+
return this->GetColumnTextAsync(columnResultID, *columnID, itemInternalIndex);
184+
});
185+
186+
// The function call above might finish before this line runs,
187+
// but that doesn't matter, as the results won't be processed
188+
// until a message posted to the main thread has been handled
189+
// (which can only occur after this function has returned).
190+
m_columnResults.insert({ columnResultID, std::move(result) });
230191
}
231192

232-
void CShellBrowser::SetAllColumnText(void)
193+
CShellBrowser::ColumnResult_t CShellBrowser::GetColumnTextAsync(int columnResultId, unsigned int ColumnID, int InternalIndex) const
233194
{
234-
int ItemIndex;
235-
BOOL QueueNotEmpty = RemoveFromColumnQueue(&ItemIndex);
195+
std::wstring columnText = GetColumnText(ColumnID, InternalIndex);
236196

237-
while(QueueNotEmpty)
197+
// This message may be delivered before this function has returned.
198+
// That doesn't actually matter, since the message handler will
199+
// simply wait for the result to be returned.
200+
PostMessage(m_hListView, WM_APP_COLUMN_RESULT_READY, columnResultId, 0);
201+
202+
ColumnResult_t result;
203+
result.itemInternalIndex = InternalIndex;
204+
result.columnID = ColumnID;
205+
result.columnText = columnText;
206+
207+
return result;
208+
}
209+
210+
void CShellBrowser::ProcessColumnResult(int columnResultId)
211+
{
212+
auto itr = m_columnResults.find(columnResultId);
213+
214+
if (itr == m_columnResults.end())
215+
{
216+
// This result is for a previous folder. It can be ignored.
217+
return;
218+
}
219+
220+
auto result = itr->second.get();
221+
222+
auto index = LocateItemByInternalIndex(result.itemInternalIndex);
223+
224+
if (!index)
225+
{
226+
// This is a valid state. The item may simply have been deleted.
227+
return;
228+
}
229+
230+
auto columnIndex = GetColumnIndexById(result.columnID);
231+
232+
if (!columnIndex)
238233
{
239-
int iColumnIndex = 0;
234+
// This is also a valid state. The column may have been removed.
235+
return;
236+
}
237+
238+
auto columnText = std::make_unique<TCHAR[]>(result.columnText.size() + 1);
239+
StringCchCopy(columnText.get(), result.columnText.size() + 1, result.columnText.c_str());
240+
ListView_SetItemText(m_hListView, *index, *columnIndex, columnText.get());
240241

241-
for(auto itr = m_pActiveColumnList->begin();itr != m_pActiveColumnList->end();itr++)
242+
m_columnResults.erase(itr);
243+
}
244+
245+
boost::optional<int> CShellBrowser::GetColumnIndexById(unsigned int id) const
246+
{
247+
HWND header = ListView_GetHeader(m_hListView);
248+
249+
int numItems = Header_GetItemCount(header);
250+
251+
for (int i = 0; i < numItems; i++)
252+
{
253+
HDITEM hdItem;
254+
hdItem.mask = HDI_LPARAM;
255+
BOOL res = Header_GetItem(header, i, &hdItem);
256+
257+
if (!res)
242258
{
243-
if(itr->bChecked)
244-
{
245-
SetColumnText(itr->id,ItemIndex,iColumnIndex++);
246-
}
259+
continue;
247260
}
248261

249-
QueueNotEmpty = RemoveFromColumnQueue(&ItemIndex);
262+
if (hdItem.lParam == id)
263+
{
264+
return i;
265+
}
250266
}
251267

252-
ApplyHeaderSortArrow();
268+
return boost::none;
269+
}
270+
271+
boost::optional<unsigned int> CShellBrowser::GetColumnIdByIndex(int index) const
272+
{
273+
HWND hHeader = ListView_GetHeader(m_hListView);
274+
275+
HDITEM hdItem;
276+
hdItem.mask = HDI_LPARAM;
277+
BOOL res = Header_GetItem(hHeader, index, &hdItem);
278+
279+
if (!res)
280+
{
281+
return boost::none;
282+
}
283+
284+
return hdItem.lParam;
253285
}
254286

255287
void CShellBrowser::SetColumnText(UINT ColumnID,int ItemIndex,int ColumnIndex)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/******************************************************************
2+
*
3+
* Project: Explorer++
4+
* File: ListView.cpp
5+
* License: GPL - See LICENSE in the top level directory
6+
*
7+
* Written by David Erceg
8+
* www.explorerplusplus.com
9+
*
10+
*****************************************************************/
11+
12+
#include "stdafx.h"
13+
#include "iShellView.h"
14+
15+
LRESULT CALLBACK CShellBrowser::ListViewProcStub(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
16+
{
17+
UNREFERENCED_PARAMETER(uIdSubclass);
18+
19+
CShellBrowser *shellBrowser = reinterpret_cast<CShellBrowser *>(dwRefData);
20+
return shellBrowser->ListViewProc(hwnd, uMsg, wParam, lParam);
21+
}
22+
23+
LRESULT CALLBACK CShellBrowser::ListViewProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
24+
{
25+
switch (uMsg)
26+
{
27+
case WM_APP_COLUMN_RESULT_READY:
28+
ProcessColumnResult(static_cast<int>(wParam));
29+
break;
30+
}
31+
32+
return DefSubclassProc(hwnd, uMsg, wParam, lParam);
33+
}

Explorer++/Explorer++/ShellBrowser/iFolderView.cpp

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ m_hOwner(hOwner),
7777
m_hListView(hListView),
7878
m_hThread(hIconThread),
7979
m_hFolderSizeThread(hFolderSizeThread),
80-
m_itemIDCounter(0)
80+
m_itemIDCounter(0),
81+
m_columnThreadPool(1),
82+
m_columnResultIDCounter(0)
8183
{
8284
m_iRefCount = 1;
8385

@@ -126,7 +128,6 @@ m_itemIDCounter(0)
126128
m_nAwaitingAdd = 0;
127129

128130
InitializeCriticalSection(&m_csDirectoryAltered);
129-
InitializeCriticalSection(&m_column_cs);
130131
InitializeCriticalSection(&m_folder_cs);
131132

132133
if(!g_bcsThumbnailInitialized)
@@ -148,17 +149,24 @@ m_itemIDCounter(0)
148149
}
149150

150151
m_hIconEvent = CreateEvent(NULL,TRUE,TRUE,NULL);
151-
m_hColumnQueueEvent = CreateEvent(NULL,TRUE,TRUE,NULL);
152152
m_hFolderQueueEvent = CreateEvent(NULL,TRUE,TRUE,NULL);
153+
154+
m_ListViewSubclassed = SetWindowSubclass(hListView, ListViewProcStub, LISTVIEW_SUBCLASS_ID, reinterpret_cast<DWORD_PTR>(this));
153155
}
154156

155157
CShellBrowser::~CShellBrowser()
156158
{
159+
if (m_ListViewSubclassed)
160+
{
161+
RemoveWindowSubclass(m_hListView, ListViewProcStub, LISTVIEW_SUBCLASS_ID);
162+
}
163+
157164
EmptyIconFinderQueue();
158165
EmptyThumbnailsQueue();
159-
EmptyColumnQueue();
160166
EmptyFolderQueue();
161167

168+
m_columnThreadPool.clear_queue();
169+
162170
/* Wait for any current processing to finish. */
163171
WaitForSingleObject(m_hIconEvent,INFINITE);
164172

@@ -167,7 +175,6 @@ CShellBrowser::~CShellBrowser()
167175
m_pDragSourceHelper->Release();
168176

169177
DeleteCriticalSection(&m_folder_cs);
170-
DeleteCriticalSection(&m_column_cs);
171178
DeleteCriticalSection(&m_csDirectoryAltered);
172179

173180
int nItems = ListView_GetItemCount(m_hListView);
@@ -229,13 +236,6 @@ void CShellBrowser::SetCurrentViewMode(UINT ViewMode)
229236
{
230237
case VM_DETAILS:
231238
{
232-
int i = 0;
233-
234-
for(i = 0;i < m_nTotalItems;i++)
235-
AddToColumnQueue(i);
236-
237-
QueueUserAPC(SetAllColumnDataAPC,m_hThread,(ULONG_PTR)this);
238-
239239
if(m_bShowFolderSizes)
240240
QueueUserAPC(SetAllFolderSizeColumnDataAPC,m_hFolderSizeThread,(ULONG_PTR)this);
241241
}

Explorer++/Explorer++/ShellBrowser/iShellBrowser.cpp

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,21 @@ int CShellBrowser::LocateFileItemInternalIndex(const TCHAR *szFileName) const
265265
return -1;
266266
}
267267

268+
boost::optional<int> CShellBrowser::LocateItemByInternalIndex(int internalIndex) const
269+
{
270+
LVFINDINFO lvfi;
271+
lvfi.flags = LVFI_PARAM;
272+
lvfi.lParam = internalIndex;
273+
int item = ListView_FindItem(m_hListView, -1, &lvfi);
274+
275+
if (item == -1)
276+
{
277+
return boost::none;
278+
}
279+
280+
return item;
281+
}
282+
268283
DWORD CShellBrowser::QueryFileAttributes(int iItem) const
269284
{
270285
LVITEM lvItem;
@@ -355,6 +370,11 @@ void CShellBrowser::OnListViewGetDisplayInfo(LPARAM lParam)
355370
return;
356371
}
357372

373+
if (m_ViewMode == VM_DETAILS && (plvItem->mask & LVIF_TEXT) == LVIF_TEXT)
374+
{
375+
QueueColumnTask(static_cast<int>(plvItem->lParam), plvItem->iSubItem);
376+
}
377+
358378
if((plvItem->mask & LVIF_IMAGE) == LVIF_IMAGE)
359379
{
360380
if((m_fileInfoMap.at(static_cast<int>(plvItem->lParam)).dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != FILE_ATTRIBUTE_DIRECTORY)
@@ -1262,7 +1282,6 @@ void CShellBrowser::SetTerminationStatus(void)
12621282
{
12631283
EmptyIconFinderQueue();
12641284
EmptyThumbnailsQueue();
1265-
EmptyColumnQueue();
12661285
EmptyFolderQueue();
12671286
m_bNotifiedOfTermination = TRUE;
12681287
}

0 commit comments

Comments
 (0)