xref: /haiku/src/apps/showimage/ImageCache.cpp (revision 5c6260dc232fcb2d4d5d1103c1623dba9663b753)
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 /*static*/ ImageCache ImageCache::sCache;
42 
43 
44 // #pragma mark -
45 
46 
47 BitmapOwner::BitmapOwner(BBitmap* bitmap)
48 	:
49 	fBitmap(bitmap)
50 {
51 }
52 
53 
54 BitmapOwner::~BitmapOwner()
55 {
56 	delete fBitmap;
57 }
58 
59 
60 // #pragma mark -
61 
62 
63 ImageCache::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 
83 ImageCache::~ImageCache()
84 {
85 	// TODO: delete CacheEntries, and QueueEntries
86 }
87 
88 
89 status_t
90 ImageCache::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
164 ImageCache::_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 
200 status_t
201 ImageCache::_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 
305 void
306 ImageCache::_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 
329 void
330 ImageCache::_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 
345 void
346 ImageCache::_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