1/*
2 * Copyright 2010-2011, Axel D��rfler, axeld@pinc-software.de.
3 * Distributed under the terms of the MIT License.
4 */
5
6
7#include "ImageCache.h"
8
9#include <new>
10
11#include <Autolock.h>
12#include <Bitmap.h>
13#include <BitmapStream.h>
14#include <Debug.h>
15#include <File.h>
16#include <Messenger.h>
17#include <TranslatorRoster.h>
18
19#include <AutoDeleter.h>
20
21#include "ShowImageConstants.h"
22
23
24//#define TRACE_CACHE
25#undef TRACE
26#ifdef TRACE_CACHE
27#	define TRACE(x, ...) printf(x, __VA_ARGS__)
28#else
29#	define TRACE(x, ...) ;
30#endif
31
32
33struct QueueEntry {
34	entry_ref				ref;
35	int32					page;
36	status_t				status;
37	std::set<BMessenger>	listeners;
38};
39
40
41/*static*/ ImageCache ImageCache::sCache;
42
43
44// #pragma mark -
45
46
47BitmapOwner::BitmapOwner(BBitmap* bitmap)
48	:
49	fBitmap(bitmap)
50{
51}
52
53
54BitmapOwner::~BitmapOwner()
55{
56	delete fBitmap;
57}
58
59
60// #pragma mark -
61
62
63ImageCache::ImageCache()
64	:
65	fLocker("image cache"),
66	fThreadCount(0),
67	fBytes(0)
68{
69	system_info info;
70	get_system_info(&info);
71
72	fMaxThreadCount = info.cpu_count - 1;
73	if (fMaxThreadCount < 1)
74		fMaxThreadCount = 1;
75	fMaxBytes = info.max_pages * B_PAGE_SIZE / 5;
76	fMaxEntries = 10;
77	TRACE("max thread count: %" B_PRId32 ", max bytes: %" B_PRIu64
78			", max entries: %" B_PRIuSIZE "\n",
79		fMaxThreadCount, fMaxBytes, fMaxEntries);
80}
81
82
83ImageCache::~ImageCache()
84{
85	// TODO: delete CacheEntries, and QueueEntries
86}
87
88
89status_t
90ImageCache::RetrieveImage(const entry_ref& ref, int32 page,
91	const BMessenger* target)
92{
93	BAutolock locker(fLocker);
94
95	CacheMap::iterator find = fCacheMap.find(std::make_pair(ref, page));
96	if (find != fCacheMap.end()) {
97		CacheEntry* entry = find->second;
98
99		// Requeue cache entry to the end of the by-age list
100		TRACE("requeue trace entry %s\n", ref.name);
101		fCacheEntriesByAge.Remove(entry);
102		fCacheEntriesByAge.Add(entry);
103
104		// Notify target, if any
105		_NotifyTarget(entry, target);
106		return B_OK;
107	}
108
109	QueueMap::iterator findQueue = fQueueMap.find(std::make_pair(ref, page));
110	QueueEntry* entry;
111
112	if (findQueue == fQueueMap.end()) {
113		if (target == NULL
114			&& ((fCacheMap.size() < 4 && fCacheMap.size() > 1
115					&& fBytes + fBytes / fCacheMap.size() > fMaxBytes)
116				|| (fMaxThreadCount == 1 && fQueueMap.size() > 1))) {
117			// Don't accept any further precaching if we're low on memory
118			// anyway, or if there is already a busy queue.
119			TRACE("ignore entry %s\n", ref.name);
120			return B_NO_MEMORY;
121		}
122
123		TRACE("add to queue %s\n", ref.name);
124
125		// Push new entry to the queue
126		entry = new(std::nothrow) QueueEntry();
127		if (entry == NULL)
128			return B_NO_MEMORY;
129
130		entry->ref = ref;
131		entry->page = page;
132
133		if (fThreadCount < fMaxThreadCount) {
134			// start a new worker thread to load the image
135			thread_id thread = spawn_thread(&ImageCache::_QueueWorkerThread,
136				"image loader", B_LOW_PRIORITY, this);
137			if (thread >= B_OK) {
138				atomic_add(&fThreadCount, 1);
139				resume_thread(thread);
140			} else if (fThreadCount == 0) {
141				delete entry;
142				return thread;
143			}
144		}
145
146		fQueueMap.insert(std::make_pair(
147			std::make_pair(entry->ref, entry->page), entry));
148		fQueue.push_front(entry);
149	} else {
150		entry = findQueue->second;
151		TRACE("got entry %s from cache\n", entry->ref.name);
152	}
153
154	if (target != NULL) {
155		// Attach target as listener
156		entry->listeners.insert(*target);
157	}
158
159	return B_OK;
160}
161
162
163/*static*/ status_t
164ImageCache::_QueueWorkerThread(void* _self)
165{
166	ImageCache* self = (ImageCache*)_self;
167	TRACE("%ld: start worker thread\n", find_thread(NULL));
168
169	// get next queue entry
170	while (true) {
171		self->fLocker.Lock();
172		if (self->fQueue.empty()) {
173			self->fLocker.Unlock();
174			break;
175		}
176
177		QueueEntry* entry = *self->fQueue.begin();
178		TRACE("%ld: got entry %s from queue.\n", find_thread(NULL),
179			entry->ref.name);
180		self->fQueue.pop_front();
181		self->fLocker.Unlock();
182
183		CacheEntry* cacheEntry = NULL;
184		entry->status = self->_RetrieveImage(entry, &cacheEntry);
185
186		self->fLocker.Lock();
187		self->fQueueMap.erase(std::make_pair(entry->ref, entry->page));
188		self->_NotifyListeners(cacheEntry, entry);
189		self->fLocker.Unlock();
190
191		delete entry;
192	}
193
194	atomic_add(&self->fThreadCount, -1);
195	TRACE("%ld: end worker thread\n", find_thread(NULL));
196	return B_OK;
197}
198
199
200status_t
201ImageCache::_RetrieveImage(QueueEntry* queueEntry, CacheEntry** _entry)
202{
203	CacheEntry* entry = new(std::nothrow) CacheEntry();
204	if (entry == NULL)
205		return B_NO_MEMORY;
206
207	ObjectDeleter<CacheEntry> deleter(entry);
208
209	BTranslatorRoster* roster = BTranslatorRoster::Default();
210	if (roster == NULL)
211		return B_ERROR;
212
213	BFile file;
214	status_t status = file.SetTo(&queueEntry->ref, B_READ_ONLY);
215	if (status != B_OK)
216		return status;
217
218	translator_info info;
219	memset(&info, 0, sizeof(translator_info));
220	BMessage ioExtension;
221
222	if (queueEntry->page != 0
223		&& ioExtension.AddInt32("/documentIndex", queueEntry->page) != B_OK)
224		return B_NO_MEMORY;
225
226	// TODO: this doesn't work for images that already are in the queue...
227	if (!queueEntry->listeners.empty()) {
228		BMessage progress(kMsgImageCacheProgressUpdate);
229		progress.AddRef("ref", &queueEntry->ref);
230		ioExtension.AddMessenger("/progressMonitor",
231			*queueEntry->listeners.begin());
232		ioExtension.AddMessage("/progressMessage", &progress);
233	}
234
235	// Translate image data and create a new ShowImage window
236
237	BBitmapStream outstream;
238
239	status = roster->Identify(&file, &ioExtension, &info, 0, NULL,
240		B_TRANSLATOR_BITMAP);
241	if (status == B_OK) {
242		status = roster->Translate(&file, &info, &ioExtension, &outstream,
243			B_TRANSLATOR_BITMAP);
244	}
245	if (status != B_OK)
246		return status;
247
248	BBitmap* bitmap;
249	if (outstream.DetachBitmap(&bitmap) != B_OK)
250		return B_ERROR;
251
252	entry->bitmapOwner = new(std::nothrow) BitmapOwner(bitmap);
253	if (entry->bitmapOwner == NULL) {
254		delete bitmap;
255		return B_NO_MEMORY;
256	}
257
258	entry->ref = queueEntry->ref;
259	entry->page = queueEntry->page;
260	entry->bitmap = bitmap;
261	entry->type = info.name;
262	entry->mimeType = info.MIME;
263
264	// get the number of documents (pages) if it has been supplied
265	int32 documentCount = 0;
266	if (ioExtension.FindInt32("/documentCount", &documentCount) == B_OK
267		&& documentCount > 0)
268		entry->pageCount = documentCount;
269	else
270		entry->pageCount = 1;
271
272	deleter.Detach();
273	*_entry = entry;
274
275	BAutolock locker(fLocker);
276
277	fCacheMap.insert(std::make_pair(
278		std::make_pair(entry->ref, entry->page), entry));
279	fCacheEntriesByAge.Add(entry);
280
281	fBytes += bitmap->BitsLength();
282
283	TRACE("%ld: cached entry %s from queue (%" B_PRIu64 " bytes.\n",
284		find_thread(NULL), entry->ref.name, fBytes);
285
286	while (fBytes > fMaxBytes || fCacheMap.size() > fMaxEntries) {
287		if (fCacheMap.size() <= 2)
288			break;
289
290		// Remove the oldest entry
291		entry = fCacheEntriesByAge.RemoveHead();
292		TRACE("%ld: purge cached entry %s from queue.\n", find_thread(NULL),
293			entry->ref.name);
294		fBytes -= entry->bitmap->BitsLength();
295		fCacheMap.erase(std::make_pair(entry->ref, entry->page));
296
297		entry->bitmapOwner->ReleaseReference();
298		delete entry;
299	}
300
301	return B_OK;
302}
303
304
305void
306ImageCache::_NotifyListeners(CacheEntry* entry, QueueEntry* queueEntry)
307{
308	ASSERT(fLocker.IsLocked());
309
310	if (queueEntry->listeners.empty())
311		return;
312
313	BMessage notification(kMsgImageCacheImageLoaded);
314	_BuildNotification(entry, notification);
315
316	if (queueEntry->status != B_OK)
317		notification.AddInt32("error", queueEntry->status);
318
319	std::set<BMessenger>::iterator iterator = queueEntry->listeners.begin();
320	for (; iterator != queueEntry->listeners.end(); iterator++) {
321		if (iterator->SendMessage(&notification) == B_OK && entry != NULL) {
322			entry->bitmapOwner->AcquireReference();
323				// this is the reference owned by the target
324		}
325	}
326}
327
328
329void
330ImageCache::_NotifyTarget(CacheEntry* entry, const BMessenger* target)
331{
332	if (target == NULL)
333		return;
334
335	BMessage notification(kMsgImageCacheImageLoaded);
336	_BuildNotification(entry, notification);
337
338	if (target->SendMessage(&notification) == B_OK && entry != NULL) {
339		entry->bitmapOwner->AcquireReference();
340			// this is the reference owned by the target
341	}
342}
343
344
345void
346ImageCache::_BuildNotification(CacheEntry* entry, BMessage& message)
347{
348	if (entry == NULL)
349		return;
350
351	message.AddString("type", entry->type);
352	message.AddString("mime", entry->mimeType);
353	message.AddRef("ref", &entry->ref);
354	message.AddInt32("page", entry->page);
355	message.AddInt32("pageCount", entry->pageCount);
356	message.AddPointer("bitmap", (void*)entry->bitmap);
357	message.AddPointer("bitmapOwner", (void*)entry->bitmapOwner);
358}
359