xref: /haiku/src/apps/showimage/ImageCache.cpp (revision ed24eb5ff12640d052171c6a7feba37fab8a75d1)
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 
33 struct 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 
44 BitmapOwner::BitmapOwner(BBitmap* bitmap)
45 	:
46 	fBitmap(bitmap)
47 {
48 }
49 
50 
51 BitmapOwner::~BitmapOwner()
52 {
53 	delete fBitmap;
54 }
55 
56 
57 // #pragma mark -
58 
59 
60 ImageCache::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 
80 ImageCache::~ImageCache()
81 {
82 	Stop();
83 }
84 
85 
86 status_t
87 ImageCache::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 
160 void
161 ImageCache::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
184 ImageCache::_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 
220 status_t
221 ImageCache::_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 
325 void
326 ImageCache::_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 
349 void
350 ImageCache::_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 
365 void
366 ImageCache::_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