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